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の修正を行うと、テストがエラーになるので有効化されたユーザーのみテスト対象となるように修正する