v-crn Code Log

主に備忘録

Rails Tutorial 第4版 第7章 演習 解答例

f:id:v-crn:20190711155257p:plain

この文書はRails Tutorial 第4版 第7章 ユーザー登録の演習に対する個人の解答例です。解答には誤りや不適切な表現が含まれていることがありますが、もし誤謬を見つけたらコメント頂けると嬉しいです。
それでは、やっていきましょう!

7.1 ユーザーを表示する

7.1.1 デバッグRails環境

1. ブラウザから /about にアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか? paramsの内容から確認してみましょう。

Aboutページ下部に表示されたparamsの内容を確認します。

f:id:v-crn:20190801173943p:plain

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: static_pages
  action: about
permitted: false

この内容から、Aboutページを表示するとき、static_pagesコントローラのaboutアクションが使われていたことがわかります。

2. Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。その後、puts user.attributes.to_yamlを実行すると何が表示されますか? ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。

>> user = User.first
  User Load (0.5ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-07-26 10:03:57", updated_at: "2019-07-26 10:03:57", password_digest: "$2a$12$pbYTT6aNzZj7Be26vSbtRuqAA59NgPEY5QAA34BMYpk...">
>> puts user.attributes.to_yaml
---
id: 1
name: Michael Hartl
email: mhartl@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2019-07-26 10:03:57.479399000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2019-07-26 10:03:57.479399000 Z
  zone: *2
  time: *3
password_digest: "$2a$12$pbYTT6aNzZj7Be26vSbtRuqAA59NgPEY5QAA34BMYpk7CKubgZTXS"
=> nil
>> y user.attributes
---
id: 1
name: Michael Hartl
email: mhartl@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2019-07-26 10:03:57.479399000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2019-07-26 10:03:57.479399000 Z
  zone: *2
  time: *3
password_digest: "$2a$12$pbYTT6aNzZj7Be26vSbtRuqAA59NgPEY5QAA34BMYpk7CKubgZTXS"
=> nil

puts user.attributes.to_yamly user.attributesのコマンド実行結果は同じでした。

※そもそもyメソッドは何なのかというと、YAML形式でオブジェクトの中身を確認するメソッドです。YAMLとは、YAML Ain't Markup Languageの略で、構造化されたデータを表現するためのフォーマットの一つです。

7.1.2 Usersリソース

1. 埋め込みRubyを使って、マジックカラム (created_atとupdated_at) の値をshowページに表示してみましょう (リスト 7.4)。

app/views/users/show.html.erb

<%= @user.name %>, <%= @user.email %>
<br>
created at <%= @user.created_at %>
<br>
updated at <%= @user.updated_at %>

2. 埋め込みRubyを使って、Time.nowの結果をshowページに表示してみましょう。ページを更新すると、その結果はどう変わっていますか? 確認してみてください。

app/views/users/show.html.erb

<%= @user.name %>, <%= @user.email %>
<br>
created at <%= @user.created_at %>
<br>
updated at <%= @user.updated_at %>
<br>
Time.now ... <%= Time.now %>

Time.nowの表示は現在時刻に合わせて変化します。

7.1.3 debuggerメソッド

1. showアクションの中にdebuggerを差し込み (リスト 7.6)、ブラウザから /users/1 にアクセスしてみましょう。その後コンソールに移り、putsメソッドを使ってparamsハッシュの中身をYAML形式で表示してみましょう。ヒント: 7.1.1.1の演習を参考にしてください。その演習ではdebugメソッドで表示したデバッグ情報を、どのようにしてYAML形式で表示していたでしょうか?

(byebug) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false
nil

2. newアクションの中にdebuggerを差し込み、/users/new にアクセスしてみましょう。@userの内容はどのようになっているでしょうか? 確認してみてください。

(byebug) @user
nil

7.1.4 Gravatar画像とサイドバー

1. (任意) Gravatar上にアカウントを作成し、あなたのメールアドレスと適当な画像を紐付けてみてください。メールアドレスをMD5ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試してみましょう。

>> User.create(name: "varcyrano", email: "var.cyrano@gmail.com", password: "abc123", password_confirmation: "abc123")
   (0.7ms)  begin transaction
  User Exists (0.8ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "var.cyrano@gmail.com"], ["LIMIT", 1]]
  User Create (1.9ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?)  [["name", "varcyrano"], ["email", "var.cyrano@gmail.com"], ["created_at", "2019-07-30 06:07:44.361200"], ["updated_at", "2019-07-30 06:07:44.361200"], ["password_digest", "$2a$12$NLpbERq4USDB2WPT5worxeJtT3JqE6RywPsCW5ihp/hcG0Of0zXxu"]]
   (1.4ms)  commit transaction
=> #<User id: 3, name: "varcyrano", email: "var.cyrano@gmail.com", created_at: "2019-07-30 06:07:44", updated_at: "2019-07-30 06:07:44", password_digest: "$2a$12$NLpbERq4USDB2WPT5worxeJtT3JqE6RywPsCW5ihp/h...">

ユーザー詳細ページにアクセスすると下記のように画像がきちんと表示されることが確認できます。

f:id:v-crn:20190801174036p:plain

2. 7.1.4で定義したgravatar_forヘルパーをリスト 7.12のように変更して、sizeをオプション引数として受け取れるようにしてみましょう。うまく変更できると、gravatar_for user, size: 50といった呼び出し方ができるようになります。重要: この改善したヘルパーは10.3.1で実際に使います。忘れずに実装しておきましょう。

リスト 7.12: gravatar_forヘルパーにオプション引数を追加する
app/helpers/users_helper.rb

module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

3. オプション引数は今でもRubyコミュニティで一般的に使われていますが、Ruby 2.0から導入された新機能「キーワード引数 (Keyword Arguments)」でも実現することができます。先ほど変更したリスト 7.12を、リスト 7.13のように置き換えてもうまく動くことを確認してみましょう。この2つの実装方法はどういった違いがあるのでしょうか? 考えてみてください。

リスト 7.13: gravatar_forヘルパーにキーワード引数を追加する
app/helpers/users_helper.rb

module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, size: 80)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

キーワード引数を使うと、新しく変数を用意しなくても引数名を直接利用できます。

7.2 ユーザー登録フォーム

7.2.1 form_forを使用する

1. 試しに、リスト 7.15にある:nameを:nomeに置き換えてみましょう。どんなエラーメッセージが表示されるようになりますか?

NoMethodError in Users#new Showing /Users/***/Rails-Tutorial/sample_app/app/views/users/new.html.erb where line #8 raised:

undefined method `nome' for #<User:0x00007fc2617b6d78>

2. 試しに、ブロックの変数fをすべてfoobarに置き換えてみて、結果が変わらないことを確認してみてください。確かに結果は変わりませんが、変数名をfoobarとするのはあまり良い変更ではなさそうですね。その理由について考えてみてください。

フォーム画面に使われる変数fは"form"の頭文字に由来しています。"foobar"は意味を持たない名前として使われるため、フォームオブジェクトという明確な意味を持つ今回のような場合に使用するのは不適です。

7.2.2 フォームHTML

1. Learn Enough HTML to Be DangerousではHTMLをすべて手動で書き起こしていますが、なぜformタグを使わなかったのでしょうか? 理由を考えてみてください。

入力フォームがないから。

7.3 ユーザー登録失敗

7.3.1 正しいフォーム

演習なし。

7.3.2 Strong Parameters

1. /signup?admin=1 にアクセスし、paramsの中にadmin属性が含まれていることをデバッグ情報から確認してみましょう。

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  admin: '1'
  controller: users
  action: new
permitted: false

7.3.3 エラーメッセージ

1. 最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。

app/models/user.rb

...
validates :password, presence: true, length: { minimum: 5 }
...

パスワードの文字数を5文字未満にしてフォームを送信すると次のエラーメッセージが表示されます。

f:id:v-crn:20190801174218p:plain

これでエラーメッセージが自動的に更新されることが確認できました。

2. 未送信のユーザー登録フォーム (図 7.12) のURLと、送信済みのユーザー登録フォーム (図 7.18) のURLを比べてみましょう。なぜURLは違っているのでしょうか? 考えてみてください。

config/routes.rbにおいて
未送信のユーザー登録フォームのURL(http://localhost:3000/signup)はget '/signup', to:'users#new'に、
送信済みのユーザー登録フォームのURL(http://localhost:3000/users)はresources :usersに関連付けられている。
したがってフォームを送信するとき、signupパスからUsersControllerのcreateアクションが呼ばれ、usersパスに遷移するためフォームの送信前後でURLが異なる。

7.3.4 失敗時のテスト

1. リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
    test "invalid signup information" do
        get signup_path
        assert_no_difference 'User.count' do
            post users_path, params: { user: {name: "",
                                                        email: "user@invalid",
                                                        password: "foo",
                                                        password_confirmation: "bar" }}
        end
        assert_template 'users/new'
        assert_select ' div#error_explanation'
        assert_select 'div.alert'
    end
end

2. ユーザー登録フォームのURLは /signup ですが、無効なユーザー登録データを送付するとURLが /users に変わってしまいます。これはリスト 5.43で追加した名前付きルート (/signup) と、RESTfulなルーティング (リスト 7.3) のデフォルト設定との差異によって生じた結果です。リスト 7.26とリスト 7.27の内容を参考に、この問題を解決してみてください。うまくいけばどちらのURLも /signup になるはずです。あれ、でもテストは greenのままになっていますね...、なぜでしょうか? (考えてみてください)

リスト7.26とリスト7.27の内容を反映させた後、サーバーを再起動するとフォーム送信前後でURLは不変となります。
テストがgreenのままなのは、テストで使用しているパスがusers_pathのままになっているからです。4番目の演習で実際にそれを確かめることになります。

3. リスト 7.25のpost部分を変更して、先ほどの演習課題で作った新しいURL (/signup) に合わせてみましょう。また、テストが greenのままになっている点も確認してください。

test/integration/users_signup_test.rbのget users_pathget signup_pathに変更します。

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
    test "invalid signup information" do
        get signup_path
        assert_no_difference 'User.count' do
            post signup_path, params: { user: {name: "",
                                                        email: "user@invalid",
                                                        password: "foo",
                                                        password_confirmation: "bar" }}
        end
        assert_template 'users/new'
        assert_select ' div#error_explanation'
        assert_select 'div.alert'
    end
end

testを実行するとgreenとなります。

4. リスト 7.27のフォームを以前の状態 (リスト 7.20) に戻してみて、テストがやはり greenになっていることを確認してください。これは問題です! なぜなら、現在postが送信されているURLは正しくないのですから。assert_selectを使ったテストをリスト 7.25に追加し、このバグを検知できるようにしてみましょう (テストを追加して redになれば成功です)。その後、変更後のフォーム (リスト 7.27) に戻してみて、テストが green になることを確認してみましょう。ヒント: フォームから送信してテストするのではなく、’form[action="/signup"]’という部分が存在するかどうかに着目してテストしてみましょう。

form[action="/signup"]という記述がHTMLに含まれているかを確認します。

test/integration/users_signup_test.rb

require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
    test "invalid signup information" do
        get signup_path
        assert_no_difference 'User.count' do
            post signup_path, params: { user: {name: "",
                                                        email: "user@invalid",
                                                        password: "foo",
                                                        password_confirmation: "bar" }}
        end
        assert_template 'users/new'
        assert_select ' div#error_explanation'
        assert_select 'div.alert'
        assert_select 'form[action="/signup"]'
    end
end

テストを実行するとredになります。

$ rails t
Running via Spring preloader in process 19489
Started with run options --seed 27929

 FAIL["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x00007fe0576d4ba8 @name="UsersSignupTest">, 0.877714999995078]
 test_invalid_signup_information#UsersSignupTest (0.88s)
        Expected at least 1 element matching "form[action="/signup"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:15:in `block in <class:UsersSignupTest>'

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

Finished in 0.92986s
18 tests, 38 assertions, 1 failures, 0 errors, 0 skips

app/views/users/new.html.erbのform_forに関する記述を<%= form_for(@user, url: signup_path) do |f| %>に戻して再度テストを実行するとgreenになります。

7.4 ユーザー登録成功

7.4.1 登録フォームの完成

1. 有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認してみましょう。

http://localhost:3000/signup でユーザー登録を完了した後にRailsコンソールで作成したユーザーを確認する。

>> user = User.last
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 4, name: "name", email: "email@email.email", created_at: "2019-07-30 12:35:52", updated_at: "2019-07-30 12:35:52", password_digest: "$2a$12$xXqbugf6THLtPkVkY8O9LuOJITEAQnPYFnat2W5Ueh4...">

2. リスト 7.28を更新し、redirect_to user_url(@user)とredirect_to @userが同じ結果になることを確認してみましょう。

確認してみましょう。

7.4.2 flash

演習なし。

7.4.3 実際のユーザー登録

演習なし。

7.4.4 成功時のテスト

演習なし。

ここでリスト7.26 test/integration/users_signup_test.rb について修正事項があります。
post_via_redirectメソッドはRails 5.1以降では使用できなくなったため、下記のようにfollow_redirect!メソッドを使用してリダイレクトするように変更します。

test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
        post users_path, params: { user: { name:  "Example User",
            email: "user@example.com",
            password:              "password",
            password_confirmation: "password" }}
        follow_redirect!
    end
    assert_template 'users/show'
end

7.5 プロのデプロイ

7.5.1 本番環境でのSSL

演習なし。

7.5.2 本番環境用Webサーバー

演習なし。

リスト7.30の後のbundle exec rake testコマンドでエラーが出てきたら7.4.4の修正事項に対応していない可能性があるので、確認してください。

7.5.3 Rubyのバージョン番号

演習なし。

7.6 最後に

7.6.1 本章のまとめ

演習なし。

7.7 演習

1. リスト7.31のコードを使用して、7.1.4で定義されたgravatar_forヘルパーにオプションのsizeパラメーターを取ることができる (gravatar_for user, size: 40のようなコードをビューで使用できる) ことを確認してください。(9.3.1でこれを改善したヘルパーを使います)

確認してください。

2. リスト7.18で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト7.32にテンプレートを用意しておいたので、参考にしてください。

test/integration/users_signup_test.rb

test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
        post users_path, params: { user: {name: "",
                                                    email: "user@invalid",
                                                    password: "foo",
                                                    password_confirmation: "bar" }}
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
    assert_select 'ul' do
        assert_select 'li', 'Name can\'t be blank'
        assert_select 'li', 'Email is invalid'
        assert_select 'li', 'Password confirmation doesn\'t match Password'
        assert_select 'li', 'Password is too short (minimum is 6 characters)'
    end
end

3. 7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。 リスト7.33に最小限のテンプレートを用意しておいたので、参考にしてください (ヒント: FILL_INメソッドを適切なコードに置き換えると完成します)。(テキストに対するテストは壊れやすいです。文量の少ないflashのキーであっても、それは同じです。個人的には、flashが空でないかをテストするだけの場合が多いです)

test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
        post users_path, params: { user: { name:  "Example User",
            email: "user@example.com",
            password:              "password",
            password_confirmation: "password" }}
        follow_redirect!
    end
    assert_template 'users/show'
    assert_not flash.empty?
end

4. 7.4.2で触れたように、flash用のHTML (リスト7.25) は読みにくいです。より読みやすくしたリスト7.34のコードに対してテストスイートを実行し、こちらも正常に動作することを確認してください。このコードでは、Railsのcontent_tagヘルパーを使用しています。

確認してください。