おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

Ruby on Rails チュートリアルで30歳までに人生を変える(第11章)

こんにちは。opiyoです。

今回は、第11章をやっていきます。

第11章はメールを使ってアカウントを有効化する方法です。

どうやら難しそうですが、早速行ってみましょう。

railsチュートリアル11章の演習解説

11.1.1 演習 AccountActivationsコントローラ

11.1.1.1

<問題>現時点でテストスイートを実行すると greenになることを確認してみましょう。

<回答> 問題ないことを確認

11.1.1.1

<問題>表 11.2の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。

<回答> _pathだと、相対パスでの表記になってしまうため。

11.1.2 演習 AccountActivationのデータモデル

11.1.2.1

<問題>本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。

<回答> 問題ないことを確認

11.1.2.2

<問題>コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

<回答>

> u = User.last
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fcb594c0f50
 id: 101,
 name: "haku",
 email: "haku@co.jp",
 created_at: Mon, 07 Aug 2017 03:43:38 UTC +00:00,
 updated_at: Mon, 07 Aug 2017 03:43:38 UTC +00:00,
 password_digest: "$2a$10$A3jl3TpJQRtjO4ENd.vAA.8X.hpGb0v/NpDjcBmvHXw0HdGZGN5h6",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$PY8DTSnaULLELaVLIF/agOae7WOsh/gZFfS0U9cGYBRQnCqQR3vby",
 activated: false,
 activated_at: nil>
[22] pry(main)> u.create_activation_digest
NoMethodError: private method `create_activation_digest' called for #<User:0x007fcb594c0f50>
Did you mean?  created_at_change
               created_at_previous_change
               created_at
               created_at_was
               created_at_changed?
               created_at_will_change!
               restore_activation_digest!
from /Users/taku/.rbenv/versions/2.3.4/lib/ruby/gems/2.3.0/gems/activemodel-5.0.0.1/lib/active_model/attribute_methods.rb:430:in `method_missing'
[23] pry(main)> u.activation_digest
=> "$2a$10$PY8DTSnaULLELaVLIF/agOae7WOsh/gZFfS0U9cGYBRQnCqQR3vby"

11.1.2.3

<問題>リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

<回答>

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save { email.downcase! }

  private

  # def downcase_email
  #   self.email = email.downcase
  # end

end

11.2.1 演習 送信メールのテンプレート

11.2.1.1

<問題>コンソールを開き、CGIモジュールのescapeメソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don’t panic!"をエスケープすると、どんな結果になりますか?

<回答>

> CGI.escape('foo@example.com')
=> "foo%40example.com"
[5] pry(main)> CGI.escape("Don’t panic!")
=> "Don%E2%80%99t+panic%21"
> CGI.escape("!#$%&'()0=~|`{+*}<>?_")
=> "%21%23%24%25%26%27%28%290%3D%7E%7C%60%7B%2B%2A%7D%3C%3E%3F_"

11.2.2 演習 送信メールのプレビュー

11.2.2.1

<問題>Railsプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?

<回答>

Date: Fri, 25 Aug 2017 08:35:47 +0000

http://localhost:3000/rails/mailers/user_mailer/account_activationをブラウザで表示した時間が表示される。

11.2.3 演習 送信メールのテスト

11.2.3.1

<問題>この時点で、テストスイートが greenになっていることを確認してみましょう。

<回答> 問題ないことを確認した。

<問題>リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが redに変わることを確認してみましょう。

<回答>

 FAIL["test_account_activation", UserMailerTest, 5.321151973999804]
 test_account_activation#UserMailerTest (5.32s)
        Expected /michael@example\.com/ to match # encoding: US-ASCII
        "\r\n----==_mimepart_599fe73745950_18423fe6edc3fa14170d5\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHi Michael Example,\r\n\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n\r\nhttp://localhost:3000/account_activations/8LjBfLrHo_srJ_DuuIXlRw/edit?email=michael%40example.com\r\n\r\n\r\n----==_mimepart_599fe73745950_18423fe6edc3fa14170d5\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n  <head>\r\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n    <style>\r\n      /* Email styles need to be inline */\r\n    </style>\r\n  </head>\r\n\r\n  <body>\r\n    <h1>Sample App</h1>\r\n\r\n<p>Hi Michael Example,</p>\r\n\r\n<p>\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n</p>\r\n\r\n<a href=\"http://localhost:3000/account_activations/8LjBfLrHo_srJ_DuuIXlRw/edit?email=michael%40example.com\">Activate</a>\r\n\r\n  </body>\r\n</html>\r\n\r\n----==_mimepart_599fe73745950_18423fe6edc3fa14170d5--\r\n".
        test/mailers/user_mailer_test.rb:15:in `block in <class:UserMailerTest>'
(byebug) user.email
"michael@example.com" # `@`がそのまま表示される!
(byebug) CGI.escape(user.email)
"michael%40example.com"

11.2.4 演習 ユーザーのcreateアクションを更新

11.2.4.1

<問題>新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?

<回答> - http://localhost:3000/にリダイレクトされることを確認。 - <a href="http://localhost:3000/account_activations/GworqOheQT8myuVYkpf3hw/edit?email=taku%40co.jp">Activate</a>

<問題>コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。

<回答>

> User.last
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fcb535b1cd0
 id: 104,
 name: "taku",
 email: "taku@co.jp",
 created_at: Fri, 25 Aug 2017 09:22:39 UTC +00:00,
 updated_at: Fri, 25 Aug 2017 09:22:39 UTC +00:00,
 password_digest: "$2a$10$9Txlyza5j1zT5lmL1ldaJ.5ukrmPGHDYsuF6xCSDB6UJ0AdTPbqn.",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$k8NN5m1heGxd9K5t4lsd8OI2UHeTMSVhZcyllq7K9sIDpQQJYDgDu", # トークンは設定されている
 activated: false, # falseになってる
 activated_at: nil>

11.3.1 演習 authenticated?メソッドの抽象化

11.3.1.1

<問題>コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?

<回答>

> User.create!(name:  "13-1",
[2] pry(main)*   email: "13-1@railstutorial.org",
[2] pry(main)*   password:              "foobar",
[2] pry(main)*   password_confirmation: "foobar",
[2] pry(main)*   admin: true,
[2] pry(main)*   activated: true,
[2] pry(main)* activated_at: Time.zone.now)
   (0.1ms)  begin transaction
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "13-1@railstutorial.org"], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "admin", "activation_digest", "activated", "activated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)  [["name", "13-1"], ["email", "13-1@railstutorial.org"], ["created_at", 2017-08-25 09:39:28 UTC], ["updated_at", 2017-08-25 09:39:28 UTC], ["password_digest", "$2a$10$60JFHnETt90jy9lIclanOuEwrBLJcJUd71B.iWjMYHrXE/v5VylLy"], ["admin", true], ["activation_digest", "$2a$10$BQV6wUIYXaaVlZkuIcC8HOYD0qllFBohGUBM9Ib3jmAhfVMRMVqMe"], ["activated", true], ["activated_at", 2017-08-25 09:39:27 UTC]]
   (2.7ms)  commit transaction
=> #<User:0x007fcb546f9ce0
 id: 105,
 name: "13-1",
 email: "13-1@railstutorial.org",
 created_at: Fri, 25 Aug 2017 09:39:28 UTC +00:00,
 updated_at: Fri, 25 Aug 2017 09:39:28 UTC +00:00,
 password_digest: "$2a$10$60JFHnETt90jy9lIclanOuEwrBLJcJUd71B.iWjMYHrXE/v5VylLy",
 remember_digest: nil,
 admin: true,
 activation_digest: "$2a$10$BQV6wUIYXaaVlZkuIcC8HOYD0qllFBohGUBM9Ib3jmAhfVMRMVqMe",
 activated: true,
 activated_at: Fri, 25 Aug 2017 09:39:27 UTC +00:00>

<問題>リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。

<回答> 問題ないことを確認した

11.3.2 演習 editアクションで有効化

11.3.2.1

<問題>コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?

<回答>

http://localhost:3000/account_activations/466u-4hqImyNdeVsrhPbKw/edit?email=haku%40co.jp

466u-4hqImyNdeVsrhPbKwこの部分が有効化トーク

11.3.2.2

<問題>先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。

<回答>

# 有効化前
> User.last
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fa9f1ec3e78
 id: 102,
 name: "toma",
 email: "toma@co.jp",
 created_at: Sat, 26 Aug 2017 13:50:08 UTC +00:00,
 updated_at: Sat, 26 Aug 2017 13:50:08 UTC +00:00,
 password_digest: "$2a$10$A6P5nYGWenFVG6b23WZbi.G8GFHj7HZi7YfsDxoT0C/FMi/86QoZ6",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$cIGjCz5OVXBxeJ3QUC56ouStDhT6InDg087d6BaKh2A0/zGlh6sLK",
 activated: false,
# 有効化後
[14] pry(main)> User.last
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x007fa9f1d462a8
 id: 102,
 name: "toma",
 email: "toma@co.jp",
 created_at: Sat, 26 Aug 2017 13:50:08 UTC +00:00,
 updated_at: Sat, 26 Aug 2017 13:50:18 UTC +00:00,
 password_digest: "$2a$10$A6P5nYGWenFVG6b23WZbi.G8GFHj7HZi7YfsDxoT0C/FMi/86QoZ6",
 remember_digest: nil,
 admin: false,
 activation_digest: "$2a$10$cIGjCz5OVXBxeJ3QUC56ouStDhT6InDg087d6BaKh2A0/zGlh6sLK",
 activated: true,
 activated_at: Sat, 26 Aug 2017 13:50:18 UTC +00:00>

11.3.3 演習 有効化のテストとリファクタリング

11.3.3.1

<問題>リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。

<回答>

# app/models/user.rb
  def activate
    update_attributes(activated: true, activated_at: Time.zone.now)
  end

テストが成功することも確認した

11.3.3.2

<問題>現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。

<回答>

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  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
  .
end

11.3.3.3

<問題>ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。

<回答>

# test/integration/users_index_test.rb
class UsersIndexTest < ActionDispatch::IntegrationTest

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.where(activated: true).paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end
end

11.3.3.2の修正を行うと、テストがエラーになるので有効化されたユーザーのみテスト対象となるように修正する