【Ruby on Rails】Rails チュートリアルを進めて詰まった点や勉強になった点をひたすら書いていく

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

サトゥー

目標: 頑張って2周します

題名通り、詰まった点とか勉強になった点を主に書いていきます。

Rails チュートリアルとは?何をするの?

参考 Ruby on Rails チュートリアル 実例を使ってRailsを学ぼう

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

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

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

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

  • 2019/07/14 に開始
  • Ruby on Rails って何?という状態
  • Python で競プロやってたのでコードを見ることに抵抗は無いが Ruby ほぼ知らない状態

第1章 ゼロからデプロイまで(所要時間: 3h)

この章では、文字通り簡単なアプリをゼロから作ってデプロイ(アプリとして使えるようにする)まで一気に駆け抜けます。

サトゥー

Hello World! みたいなのが表示されるだけだから怖がらないでね
この章で学ぶこと
  • Rails とはなにか?
  • AWS Cloud9 の環境構築
  • MVC モデルの説明
  • Git を使ったバージョン管理
  • Heroku へのデプロイ

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アプリケーションの簡単な説明
Rakefilerake コマンドで使えるタスク
Gemfileこのアプリケーションに必要な gem の定義ファイル
Gemfile.lockアプリケーションで使われる gem のバージョンを確認するためのリスト
config.ruRack ミドルウェア用の設定ファイル
.gitignoreGit に取り込みたくないファイルを指定するためのパターン

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

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

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

GETWeb 上のデータを読み取るときに使う
POSTページ上のフォームに入力した値をブラウザから送信するときに使う
PATCHサーバー上の何かを更新するときに使う
DELETEサーバー上の何かを削除するときに使う

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

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

この章で学ぶこと
  • RED, GREEN, REFACTOR サイクル
  • テスト駆動開発(TDD)
  • 埋め込み Ruby(Embedded Ruby)

サトゥー

Ruby ではクラス名にキャメルケースを使い、ファイル名にスネークケースを使う慣習があるそうです。

サトゥー

.erb っていう拡張子は埋め込み Ruby のこと。
  • <% ... %> は中に書かれたコードを単に実行するだけで何も出力しない
  • <%= ... %> は中のコードの実行結果がテンプレート(ビュー)のその部分に挿入される

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

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

サトゥー

Python も中途半端にやっているので文法の細かいところがごちゃごちゃになりそう… Ruby も結構便利な書き方たくさんあるなぁ

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

[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 = "exam@exam.com"
=> "exam@exam.com"
>> exam.full_name
=> "Michael Hartl"
>> exam.formatted_email
=> "Michael Hartl <exam@exam.com>"
>> 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 フレームワークを使うと、いい感じのデザインをすばやく実装できる

layouts/_rails_default.html.erb として作成

[_rails_defauls.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 メソッドで、モデルに対してセキュアなパスワードを追加する

foo@bar..com を許容してしまっている様子

foo@bar..com も除外できた 🎉🎉

>> u = User.new(name: "Satoooh", email: "email@email.com")
=> #<User id: nil, name: "Satoooh", email: "email@email.com", 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@email.com"], ["LIMIT", 1]]
=> false
>> u.errors.messages
=> {:password=>["can't be blank"]}

>> 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)"]}

>> user = User.find_by(email: "mhartl@example.com")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "mhartl@example.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", 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", "mhartl@example.com"], ["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 の導入

/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: "mhartl@example.com", 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: mhartl@example.com
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: mhartl@example.com
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

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

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

$ 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)

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

この章で学ぶこと
  • セッションの実装
  • ログイン・ログアウト機能の実装

>> 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: "example@railstutorial.org", 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

>> 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: "example@railstutorial.org", 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: "example@railstutorial.org", 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)
  • 永続セッションは記憶トークンと記憶ダイジェストをユーザーごとに関連付けて実現する
  • 三項演算子

永続クッキー出来た🎉

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

>> 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: "example@railstutorial.org", 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

これは次のような三項演算子(ternary operator)という表現で置き換えることができる。

論理値? ? 何かをする : 別のことをする

ex 1.

if boolean?
    var = foo
  else
    var = bar
  end

var = boolean? ? foo : bar

ex 2.

def foo
  do_stuff
  boolean? ? "bar" : "baz"
end

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

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

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

サトゥー

debugger 動かしているときの予期せぬ挙動が怖かったです
この章で学ぶこと
  • フレンドリーフォワーディング
  • ページネーションの実装
  • 管理者権限の実装

gravatar の部分の <a> タグに rel="noopener" を追記。

[edit.html.erb]

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

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

[users_edit_test.rb]

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

リスト10.32 の redirect_back_or user って redirect_back_or @user じゃないのかな?

何故か 10.3.4 の最後の test でエラーが帰ってきてどこがどうおかしいのか…

ERROR["test_index_including_pagination", UsersIndexTest, 0.6274847129971022]
 test_index_including_pagination#UsersIndexTest (0.63s)
NoMethodError:         NoMethodError: undefined method `paginate' for #<Class:0x00007f9978974c80>
            app/controllers/users_controller.rb:6:in `index'
            test/integration/users_index_test.rb:11:in `block in <class:UsersIndexTest>'

調べてみると、似たような事例がありました。このページにしたがって spring stop してみると…

$ spring stop
Spring stopped.
$ rails t
Running via Spring preloader in process 31767
Started with run options --seed 27806

  39/39: [=================================================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.22118s
39 tests, 133 assertions, 0 failures, 0 errors, 0 skips

あら、無事通りましたね。

調べてみると、 spring というのは Rails のプリローダーの gem で、アプリケーションをバックグラウンドで走らせることができるようです。ただコレの影響で固まることもあるそうです。(まさに挙動がおかしかったのはこれのせいか…)

サトゥー

また一つ勉強になりました。

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

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

サトゥー

コードの分量が多く、意味を追うだけでも頭がクラクラしてきます

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

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

[user.rb]

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

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

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

[development.rb]

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

有効化トークンはこんな感じ?

https://us-east-2.console.aws.amazon.com/account_activations/TCRlsk-GBJQ_K7ILKqfOcA/edit?email=hoge%40hogehoge.com

アカウントが作られており、有効化ステータス activated: が false 担っていることが確認できる。

>> User.find_by(email: "hoge@hogehoge.com")
  User Load (1.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "hoge@hogehoge.com"], ["LIMIT", 1]]
=> #<User id: 101, name: "hogemaru", email: "hoge@hogehoge.com", created_at: "2019-07-18 11:01:20", updated_at: "2019-07-18 11:01:20", password_digest: "$2a$10$54sREh5EUFsIUOyC1jYLReu2Z1JCyLZnK5s9YS7z5qj...", remember_digest: nil, admin: false, activation_digest: "$2a$10$JIu0KDusXN.WrlSgSVCP6OWxal0FXybHXND8hHVsysg...", activated: false, activated_at: nil>

有効化トークン、有効化ダイジェストは作成されたが、記憶トークン、記憶ダイジェストは nil になっている。(有効化されてないので)

>> user = User.create(name: "satoooh", email: "satoooh@example.com", password: "foobar", password_confirmation: "foobar")
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (1.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "satoooh@example.com"], ["LIMIT", 1]]
  SQL (2.2ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "satoooh"], ["email", "satoooh@example.com"], ["created_at", "2019-07-18 11:21:49.854352"], ["updated_at", "2019-07-18 11:21:49.854352"], ["password_digest", "$2a$10$KD4ryB1TRudRvOW1ermAMu93AEjbeneVLpr4tSm2Ybxyn7BKW1AK6"], ["activation_digest", "$2a$10$o.GqSyg0Dzp93Fp3aIlKYOLUdUJoUTTPIMi4GR2R79BVQHN3F8tWS"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 102, name: "satoooh", email: "satoooh@example.com", created_at: "2019-07-18 11:21:49", updated_at: "2019-07-18 11:21:49", password_digest: "$2a$10$KD4ryB1TRudRvOW1ermAMu93AEjbeneVLpr4tSm2Ybx...", remember_digest: nil, admin: false, activation_digest: "$2a$10$o.GqSyg0Dzp93Fp3aIlKYOLUdUJoUTTPIMi4GR2R79B...", activated: false, activated_at: nil>
>> user.remember_token
=> nil
>> user.activation_token
=> "xNe2GUWHMNAxwgcE5JAVtQ"
>> user.remember_digest
=> nil
>> user.activation_digest
=> "$2a$10$o.GqSyg0Dzp93Fp3aIlKYOLUdUJoUTTPIMi4GR2R79BVQHN3F8tWS"

>> user.remember_token = User.new_token
=> "iEHJBd7652_Fwa48kUet4A"
>> user.update_attribute(:remember_digest, User.digest(user.remember_token))
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2019-07-18 11:26:35.379121"], ["remember_digest", "$2a$10$DYu4Y48bl.LuofzbeH7X6OydTKNbzKmgaqPYJ6wWRoM29rdHrlmdm"], ["id", 102]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

>> user.authenticated?(:remember, user.remember_token)
=> true

URLがこれ。

<a href="https://6963ee31c7a046909eae577b5766c0a1.vfs.cloud9.us-east-2.amazonaws.com/account_activations/BL-_dVCu-r67Fo7MsAQ-2g/edit?email=hogehoge%40hoge.com">Activate</a>

 有効化トークンは上のリンクのうち、 BL-_dVCu-r67Fo7MsAQ-2g の部分。

このリンクにアクセスすると、、

Account activated!! 🎉

コンソールで有効化ステータスが true になっていることを確認。

>> user = User.find(102)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 102], ["LIMIT", 1]]
=> #<User id: 102, name: "hogemaru", email: "hogehoge@hoge.com", created_at: "2019-07-18 11:42:35", updated_at: "2019-07-18 11:44:13", password_digest: "$2a$10$h9LMg42Vp7NAEc1CFn4Z6OswP3.rVASC8auXwEr7lLe...", remember_digest: nil, admin: false, activation_digest: "$2a$10$Risk.53IYIqX0B3OWv85wextj6Y9TLAKrDfmu3qgJ9K...", activated: true, activated_at: "2019-07-18 11:44:13">
>> user.activated
=> true

次のように書けばOK

[user.rb]

# アカウントを有効にする
def activate
  update_columns(activated: true, activated_at: Time.zone.now)
end

次の問題では、 users_controller.rb を次のように書き換える。

[users_controller.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

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

[users.yml]

non_activated:
  name: Non Activated
  email: non_activated@example.com
  password_digest: <%= User.digest('password') %>
  activated: false
  activated_at: <%= Time.zone.now %>

users_controller_test.rb にテスト作成。(ここは他サイト参考にしました、テスト自分で作るのムズくない?)

[users_controller_test.rb]

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章のメール認証によって、ユーザーのメアドが本人のものであるという確証がもてるようになっているのでがんばります。

この章で学ぶこと
  • パスワード再設定のメール送信

有効なメールアドレスをフォームから送信すると次のようになる。

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

>> user = User.find_by(email: "example@railstutorial.org")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "example@railstutorial.org"], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", 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

Date には送信した日時が載っているのかな?

Date: Thu, 18 Jul 2019 19:35:28 +0000

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

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

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

パスワード再設定前後の password_digest の値の比較。変わっていることが分かる。

>> 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: "example@railstutorial.org", 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"

1. は前章でも似たようなことをやりました。

[user.rb]

# パスワード再設定用の属性を設定する
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

2.は空欄に埋める形式で答えもほぼ書いてありますね。

[password_resets_test.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

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

[password_resets_test.rb]

    # 有効なパスワードとパスワード確認
    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
メール来た🎉😎

サトゥー

ここまできてまだ主要機能の実装入ってないのね、ログイン/ログアウトの実装って大変なんだなぁ…

参考にしたサイト

わからないとき、詰まったときはひたすらググっていたので、参考にしたサイトはここに挙げた以上に膨大にあるのですが、その中でも Rails チュートリアルの解説に特化しているものを紹介します。先人の知見に圧倒的感謝です。

Ruby on Rails チュートリアル 完全攻略 概要と演習解答総まとめ – 新米パパの育児留学

Ruby on Rails チュートリアル全まとめ(解説・単語・演習) – Qiita

コメントを残す

メールアドレスが公開されることはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)