Ruby on Railsチュートリアルの演習問題と解答をまとめる。
アウトプットすることで、より自分の理解を深めることを目的としています。
自分なりに調べて考えた回答のため、記載内容に誤りがある場合はコメントいただけると幸いです。
- 演習11.1.1 AccountActivationsコントローラ
- 演習11.1.2 AccountActivationのデータモデル
- 演習11.2.1 送信メールのテンプレート
- 演習11.2.2 送信メールのプレビュー
- 演習11.2.3 送信メールのテスト
- 演習11.2.4 ユーザーのcreateアクションを更新
- 演習11.3.1 authenticated? メソッドの抽象化
- 演習11.3.2 editアクションで有効か
- 演習11.3.3 有効化のテストとリファクタリング
- 演習11.4 本番環境でのメール送信
演習11.1.1 AccountActivationsコントローラ
問題1
現時点でテストスイートを実行すると greenになることを確認してみましょう。
解答
テストを実行し、GREENになることを確認します。
問題2
表 11.2の名前付きルートでは、pathではなくurlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。
解答
メール(外部)からアクセスしてもらうのには完全なURLが必要なため。
演習11.1.2 AccountActivationのデータモデル
問題1
本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。
解答
テスト結果はGREENのままです!
問題2
コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
解答
# Userクラスのインスタンス生成 >> user = User.first User Load (1.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-10-29 10:47:45" , updated_at: "2019-10-29 10:47:45", password_digest: "$2a$10$lAeSzwDSMQ922cNr1tSR6uAvuvp1M7mN7UTSc3k.ngN... ", remember_digest: nil, admin: true, activation_digest: "$2a$10$EWa/9y8qndhN1V1EoG2Zj.rbf8QYO62qcXNJvuQWu3h ...", activated: true, activated_at: "2019-10-29 10:47:45"> # create_activation_digestメソッド呼び出し >> user.create_activation_digest Traceback (most recent call last): 1: from (irb):2 NoMethodError (private method `create_activation_digest' called for #<User:0x00007f90e94d5518>) Did you mean? restore_activation_digest! # ダイジェストの値を確認 >> user.activation_digest => "$2a$10$EWa/9y8qndhN1V1EoG2Zj.rbf8QYO62qcXNJvuQWu3hRrQZoUy3Pm"
問題3
リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
解答
private # メールアドレスを全て小文字にする def downcase_email email.downcase! end
テストはGREENのままです!
演習11.2.1 送信メールのテンプレート
問題1
コンソールを開き、CGIモジュールのescapeメソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"をエスケープすると、どんな結果になりますか?
解答
>> CGI.escape('foo@example.com') => "foo%40example.com" >> CGI.escape("Don't panic!") => "Don%27t+panic%21"
演習11.2.2 送信メールのプレビュー
問題1
Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?
解答
[test/mailer/previews/user_mailer_preview.rb] # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation # ココ def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end
上記URLにアクセスするとプレビューを表示できる。
Date欄にはUTC時間が表示されているため日本時間とはずれている
演習11.2.3 送信メールのテスト
問題1
この時点で、テストスイートが greenになっていることを確認してみましょう。
解答
テストはGREEN
問題2
リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが redに変わることを確認してみましょう。
解答
[test/mailers/user_mailer_test.rb] require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded # assert_match CGI.escape(user.email), mail.body.encoded assert_match user.email, mail.body.encoded end end
テストがREDになる
演習11.2.4 ユーザーのcreateアクションを更新
問題1
新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
解答
リダイレクト先がroot_url
有効化トークン:"authenticity_token"=>"no3cQwkVSAvi5HBUee/8YBJJ8odHce/7VlcJQfvwQ/nosOB8eHSsSRULLRQTx5oeBc6r068aMS0ihDzKI0+bgQ=="
Started POST "/signup" for 127.0.0.1 at 2019-10-29 22:29:51 +0900 Processing by UsersController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"no3cQwkVSAvi5HBUee/8YBJJ8odHce/7VlcJQfvwQ/nosOB8eHSsSRULLRQTx5oeBc6r068aMS0ihDzKI0+bgQ==", "user"=>{"name"=>"hanzou", "email"=>"hhattori@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"} (0.1ms) begin transaction User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "hhattori@example.com"], ["LIMIT", 1]] SQL (0.6ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "hanzou"], ["email", "hhattori@example.com"], ["created_at", "2019-10-29 13:29:52.036498"], ["updated_at", "2019-10-29 13:29:52.036498"], ["password_digest", "$2a$10$ItIej6Ggpjs/Gx.AqPTjcuQmHWhc4Dw2W2//4cxQt/34jmo1onx8W"], ["activation_digest", "$2a$10$qiVAdfv5BF.LXrpEzkrdc.pCWKcx9jmCnaHaNjCDyoVWkJR3TGuy."]] (1.5ms) commit transaction Rendering user_mailer/account_activation.html.erb within layouts/mailer Rendered user_mailer/account_activation.html.erb within layouts/mailer (1.4ms) Rendering user_mailer/account_activation.text.erb within layouts/mailer Rendered user_mailer/account_activation.text.erb within layouts/mailer (0.5ms) UserMailer#account_activation: processed outbound mail in 29.5ms Sent mail to hhattori@example.com (6.4ms) Date: Tue, 29 Oct 2019 22:29:52 +0900 From: noreply@example.com To: hhattori@example.com Message-ID: <5db83ed025055_32483ffdb6c3a96c39ea@mba.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi hanzou, Welcome to the Sample App! Click on the link below to activate your account: http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com ----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840 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>Sample App</h1> <p>Hi hanzou,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com">Activate</a> </body> </html> ----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840-- Redirected to http://localhost:3000/ Completed 302 Found in 201ms (ActiveRecord: 2.4ms)
問題2
コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。
解答
ユーザーは作成されているが、activatedはfalse
>> user = User.find_by(name: "hanzou") User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "hanzou"], ["LIMIT", 1]] => #<User id: 101, name: "hanzou", email: "hhattori@example.com", created_at: "2019-10-29 13:29:52", updated_at: "2019-10-29 13:29:52", password_digest: "$2a$10$ItIej6Ggpjs/Gx.AqPTjcuQmHWhc4Dw2W2//4cxQt/3...", remember_digest: nil, admin: false, activation_digest: "$2a$10$qiVAdfv5BF.LXrpEzkrdc.pCWKcx9jmCnaHaNjCDyoV...", activated: false, activated_at: nil>
演習11.3.1 authenticated? メソッドの抽象化
問題1
コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
解答
>> user = User.create(name: "kotaro", email: "kfuma@example.com", password: "password", password_confirmatio n: "password") (0.1ms) begin transaction User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["ema il", "kfuma@example.com"], ["LIMIT", 1]] SQL (0.5ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activat ion_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "kotaro"], ["email", "kfuma@example.com"], ["created_at", "2019-10-29 13:54:15.560395"], ["updated_at", "2019-10-29 13:54:15.560395"], ["password_digest", "$2a$10$VUZ oyTovYxJzZxNA7S16YuuDPVzHA7jhYK.RvHrPuMxf3CwZTu3my"], ["activation_digest", "$2a$10$iHfMc2GlqqUJ0e.2LxL9H.Gc 62x2JDDhpfNZQFTFvbiWe4UaugiJO"]] (2.4ms) commit transaction => #<User id: 102, name: "kotaro", email: "kfuma@example.com", created_at: "2019-10-29 13:54:15", updated_at : "2019-10-29 13:54:15", password_digest: "$2a$10$VUZoyTovYxJzZxNA7S16YuuDPVzHA7jhYK.RvHrPuMx...", remember_ digest: nil, admin: false, activation_digest: "$2a$10$iHfMc2GlqqUJ0e.2LxL9H.Gc62x2JDDhpfNZQFTFvbi...", activ ated: false, activated_at: nil> >> user.remember_token => nil >> user.remember_digest => nil >> user.activation_token => "D3tmp09iJ8lqbjdeqao4Kg" >> user.activation_digest => "$2a$10$iHfMc2GlqqUJ0e.2LxL9H.Gc62x2JDDhpfNZQFTFvbiWe4UaugiJO"
演習11.3.2 editアクションで有効か
問題1
コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?
解答
Sent mail to hhattori@example.com (6.4ms) Date: Tue, 29 Oct 2019 22:29:52 +0900 From: noreply@example.com To: hhattori@example.com Message-ID: <5db83ed025055_32483ffdb6c3a96c39ea@mba.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi hanzou, Welcome to the Sample App! Click on the link below to activate your account: http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com ----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840 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>Sample App</h1> <p>Hi hanzou,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="http://localhost:3000/account_activations/_XTGFh5X4A3-5B9dryba3Q/edit?email=hhattori%40example.com">Activate</a> </body> </html> ----==_mimepart_5db83ed0240b3_32483ffdb6c3a96c3840--
この部分
account_activations/_XTGFh5X4A3-5B9dryba3Q/
問題2
先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。
解答
先ほどのURLをブラウザに貼り付けるとユーザー詳細ページにとび、「Account activated!」というフラッシュが表示される
コンソールでactivatedを確認するとtrueに変わっている
>> user = User.find_by(name: "hanzou") User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "hanzou"], ["LIMIT", 1]] => #<User id: 101, name: "hanzou", email: "hhattori@example.com", created_at: "2019-10-29 13:29:52", updated_at: "2019-10-30 12:35:42", password_digest: "$2a$10$ItIej6Ggpjs/Gx.AqPTjcuQmHWhc4Dw2W2//4cxQt/3...", remember_digest: nil, admin: false, activation_digest: "$2a$10$qiVAdfv5BF.LXrpEzkrdc.pCWKcx9jmCnaHaNjCDyoV...", activated: true, activated_at: "2019-10-30 12:35:42"> >> user.activated => true
演習11.3.3 有効化のテストとリファクタリング
問題1
リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。
解答
# アカウントを有効にする def activate update_columns(activated: true, activated_at: Time.zone.now) end
テストもGREEN!
問題2
現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。
解答
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
問題3
ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
解答
まずintegrationテストを作成する
$ rails g integration_test users_activation
テスト用のactivated: falseユーザーを新しく作る
[test/ fixtures/users.yml] red: name: Red Marlboro email: red@example.com password_digest: <%= User.digest('password') %> activated: false activated_at: <%= Time.zone.now %>
テストの流れ
setupでactivatedユーザーとnon_activatedユーザーを定義する
テスト indexページはactivatedユーザーのみ
・ログインする
・usersにアクセスする
・@userへのリンクがあることを確認
・@non_activated_userへのリンクがないことを確認
テスト showページはactivatedユーザーのみ
・ログインする
・@userの詳細ページにアクセスする
・@non_activated_userの詳細ページにアクセスする
・root_urlにリダイレクトされる
require 'test_helper' class UsersActivationTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @non_activated_user = users(:red) end test "index only activated user" do log_in_as(@user) get users_path assert_select "a[href=?]", user_path(@user) assert_select "a[href=?]", user_path(@non_activated_user), count: 0 end test "show only activated user" do log_in_as(@user) get user_path(@user) get user_path(@non_activated_user) assert_redirected_to root_url end end
テストの名前何にするかすごい迷う、、、
演習11.4 本番環境でのメール送信
問題1
実際に本番環境でユーザー登録をしてみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
解答
問題2
メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。ヒント: ターミナルからheroku logsコマンドを実行してみましょう。
解答
heroku logsコマンドをみると、有効化リンクをクリックした後にactivatedがアップデートされているのがわかる。
おしまい