v-crn Code Log

主に備忘録

🍎Railsのformに絵文字を表示する🎉

"文字列".html_safeで表示できる

HTMLで絵文字を表示したい場合、「HTML Entity」や「実体参照」と言われる形式で記述すれば普通は表示される。

😎 ...... 😎

しかしRailsのformタグ内では素直に表示してくれず、実体参照はただの文字列として認識されてしまう。
例として次のformを見てみよう。

<%= form_with url: search_path, method: :get do |f| %>
  <%= f.text_field :keywords, placeholder: "Search", class: "form-control" %>
  <%= f.submit value="&#128269;", class: "btn btn-default" %>
<% end %>

このフォームは検索欄を表している。以下の部分で検索ボタンに虫めがねの絵文字「🔍」を利用しているが、実はこの書き方では「&#128269;」と表示されてしまう。

<%= f.submit value="&#128269;", class: "btn btn-default" %>

そこでhtml_safeメソッドを利用する。

<%= f.submit value="&#128269;".html_safe, class: "btn btn-default" %>

これでRailsのHTMLの特殊文字に対するエスケープ処理がスキップされ、絵文字として出力されるようになる。

参考

HTML特殊文字のエスケープ - Ruby on Rails入門

NoMethodError | undefined method `offset' for #<Array:***> | Pagy

NoMethodError | undefined method `offset' for #<Array:***> | Pagy

状況

Pagyというgemを利用してページネーションを行うコードにおいてpagyメソッドに関する標題のエラーが発生しました。該当のコードは次のような構成です。

items = Post.where(['content LIKE ?', "%#{keyword}%"])
results = []
results += items 
@pagy, @items = pagy(items)

原因

pagyの引数はActiveRecord::Relationクラスに属している必要があります。たとえばallwhereの返り値のクラスがそれです。
今回のエラーは引数がStringの配列になっていたため発生していました。

解決策

引数をActiveRecord::Relationに変換します。

results.first.class.where(id: results.map(&:id))

一件落着!

参考

配列をActiveRecord::Relationで再取得するメソッドを作ってみる - Qiita

Railsアプリにfavicon(タブ表示アイコン)を設定する

application.html.erbでfavicon_link_tagメソッドの引数としてapp/assets/images/に用意した画像のicoファイルを指定するだけでOKです。

app/views/layouts/application.html.erb

<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= favicon_link_tag 'favicon.ico' %>

favicon_link_tagは引数なしの場合、/app/assets/images/favicon.icoを自動で読みに行ってくれます。

faviconの画像サイズは基本32×32。IE対応するなら16×16で。

OmniAuthによるTwitter認証機能の実装 | Rails | Devise | email取得

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

前提

  • Ruby 2.5.3 / Rails 5.2.3
  • DeviseでUserモデルを作成済み
  • 個々のユーザーをemailで識別する
  • Terms of ServiceとPrivacy Policyを用意できる

OmniAuthについて

TwitterFacebookなどのSNSアカウントを通じてWebアプリケーションのログイン認証を行うことができるgemです。

今回はTwitter認証のためにomniauthとomniauth-twitter、omniauth-rails_csrf_protectionという3つのgemを使用します。omniauth-rails_csrf_protectionはクロスサイトリクエストフォージェリという脆弱性への対策として導入します。

GitHub - omniauth/omniauth: OmniAuth is a flexible authentication system utilizing Rack middleware.

GitHub - cookpad/omniauth-rails_csrf_protection: Provides CSRF protection on OmniAuth request endpoint on Rails application.

GitHub - arunagw/omniauth-twitter: OmniAuth strategy for Twitter

Twitter APIの準備

Developerアカウント登録

Developer — Twitter Developers

アプリの作成

Callback URL

Callback URLs — Twitter Developers 公式によるとlocalhostはcallback URLとして使っちゃダメとのこと(何故?)。開発中はlocalhost:3000じゃなくて127.0.0.1:3000を使うと良いのか。

Don’t use localhost as a callback URL Instead of using localhost, please use a custom host locally or http(s)://127.0.0.1.

Terms of service URL / Privacy Policy URL

Twitter APIでemailを取得するにはTerms of service(利用規約) URLとPrivacy Policy URLの登録が必要。emailを扱う場合はこれらのページを準備しておきましょう。

参考

Webサービス個人開発するなら知りたい利用規約とプライバシーポリシーの作り方 - Qiita

掲示版・SNS系向きの利用規約の雛形(ひな型) | Webサイトの利用規約(無料テンプレート・商用利用可)

プライバシーポリシーの雛形(ひな型) | Webサイトの利用規約(無料テンプレート・商用利用可)

ここからRails側での作業に入ります。

Gemfile

gem 'omniauth'
gem "omniauth-rails_csrf_protection"
gem 'omniauth-twitter'

編集後、bundleを実行します。

devise.rb

Developerサイトで用意したアプリのAPIキーはアプリ詳細ページのKeys and tokens > Consumer API keysに記載されています。これらのキーをdevise.rbに設定します。

config/initializers/devise.rb

  config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY']

環境変数ENVはdotenvを利用して定義します。

.env

TWITTER_API_KEY=''
TWITTER_API_SECRET_KEY=''

忘れぬうちに本番環境にも環境変数をセットしておきましょう。

$ heroku config:set TWITTER_API_KEY=''
$ heroku config:set TWITTER_API_SECRET_KEY=''

user.rb

今回は次の要件を踏まえて実装します。 - Userモデルの作成にemailとpasswordが必須(deviseのデフォルト設定) - Twitterアカウントの表示名、アイコン画像、プロフィールをアプリのユーザーアカウントに反映させる

Usersテーブルの主なカラムは次の通り。 - email - password - name - icon - profile - provider - uid

既存のUserモデルに必要なカラムをマイグレーションで加えておきます。

$ rails g migration add_columns_to_users provider uid

deviseメソッドに:omniauthableを追加

devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :omniauthable

User.rbにfind_or_create_from_authメソッドを定義

認証時、TwitterアカウントのemailがDBのUsersテーブルに存在すればそのユーザーのオブジェクトを返し、なければTwitterアカウントの情報を用いてユーザーを新規作成、DBに保存するメソッドを定義します。この例ではオプションとしてアイコン画像とプロフィール文、SNSプロバイダー名、固有IDを引き継いでいます。

  def self.find_or_create_from_auth(auth)
    find_or_create_by(email: auth.info.email) do |user|
      user.password = Devise.friendly_token[0, 20]
      user.name = auth.info.name
      user.remote_icon_url = auth.info.image
      user.profile = auth.info.description
      user.provider = auth.provider
      user.uid = auth.uid
      user.skip_confirmation!
    end
  end
  • user.remote_icon_urlについて:carrierwaveの画像アップローダーを用意している場合、remote_画像のカラム名_urlという形式でWeb上の画像URLを格納した上でモデルのインスタンスを新規作成すると、アップローダーがリンク先から画像をダウンロードしてストレージにアップロードしてくれます。
  • user.skip_confirmation!SNS認証を通じたユーザー登録の際、メールアドレスの確認をスキップするために記述しています。ちなみにこのメソッドが行っているのは、ユーザーデータのconfirmed_atへの現在時刻の入力、保存です。

routes.rb

devise_for :users, controllers: {
  omniauth_callbacks: 'users/omniauth_callbacks'
}

omniauth_callbacks_controller

認証を行うためのコントローラーomniauth_callbacks_controllerを用意します。

$ rails g controller users/omniauth_callbacks

以下のようにコントローラーを編集します。 app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < ApplicationController
  def twitter
    callback_from :twitter
  end

  def failure
    redirect_to new_user_registration_url
  end

  private

  def callback_from(provider)
    provider = provider.to_s
    @user = User.find_or_create_from_auth(request.env['omniauth.auth'].except('extra'))

    if @user.persisted? # DBに保存済みかどうかを判定
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth'].except('extra')
      redirect_to new_user_session_url
    end
  end
end

Viewの実装

例えば以下のようにログインページやユーザー登録ページにSNS認証ボタンを設置します。 app/views/devise/registrations/new.html.erb

<h3>SNS Authentication</h3>
<%= button_to 'Twitter', user_twitter_omniauth_authorize_path %>

このとき注意しなければならないのは認証にGETメソッドを使うべきでないという点です。 クロスサイトリクエストフォージェリの対策としてプロバイダへのリンクには、link_toならmethod: :postを付ける、あるいはbutton_toを使用するなどしてPOSTリクエストを必ず使用するようにしましょう。

Test

Railsでomniauthを使ったログイン機構の統合テスト - 気持ち

とりあえず用意した簡単なテストですが一例としてはこんな感じ。

test/fixtures/users.yml

john:
  email: "john@example.com"
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
  confirmed_at: <%= Time.now - 100 %>
  provider: 'twitter'
  uid: '123456'

test/integration/users_login_test.rb

class UsersLoginTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  def setup
    Warden.test_mode!
    @user = users(:john)

    # OmniAuth configuration
    OmniAuth.config.test_mode = true
    OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new(
      provider: 'twitter',
      uid: '123456',
      info: { email: 'john@example.com', name: 'John English' }
    )
  end

  test 'login and logout with twitter account' do
    get new_user_session_path
    assert_select 'form[action=?]', user_twitter_omniauth_authorize_path
    post '/users/auth/twitter', params: OmniAuth.config.mock_auth[:twitter]
    assert_redirected_to '/users/auth/twitter/callback'
    follow_redirect!
    assert_redirected_to root_path
    follow_redirect!
    assert_select 'a[href=?]', destroy_user_session_path
    delete destroy_user_session_path
    assert_nil session[:user_id]
  end
end

認証機能の確認

ブラウザから127.0.0.1:3000にアクセスします。
問題がなければlocalhost:3000のページと同じViewが表示されるはずです。 そこからログイン画面に移動してTwitter認証リンクボタンを押してみましょう。 正常にリダイレクトされ、ログインに成功すれば勝利は目前です。

Herokuデプロイ

$ heroku pg:reset DATABASE
$ bundle exec rails assets:precompile
$ git push heroku
$ heroku run rails db:migrate

herokuへのデプロイが完了したらアプリのWeb URLにアクセスし、Twitter認証機能をチェックしてみてください。

無事、Twitter認証に成功したでしょうか?
もしそうであればあなたの勝利です。おつかれさまでした。

それともエラーに出くわしましたか?
ひとまずリフレッシュしてください。それから関連するファイルを見直したり、自分の理解した内容を整理してみましょう。諦めなければきっと解決の糸口が見つかるはずです。

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ヘルパーを使用しています。

確認してください。

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

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

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

6.1 Userモデル

6.1.1 データベースの移行

1. Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。

db/schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_07_13_160043) do

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

db/migrate/20190713160043_create_users.rb

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

schemaファイルとマイグレーションファイルとの間では、create_table "users"以下のブロックの内容がほぼ一致しています。一部異なるのはマイグレーションファイルにおけるt.timestampsがschemaファイルではt.datetime "created_at", null: falset.datetime "updated_at", null: falseに変化しているように見えることです。

2. ほぼすべてのマイグレーションは、元に戻すことが可能です (少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。$ rails db:rollback 上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください (コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。これがうまくいくのは、drop_tableとcreate_tableがそれぞれ対応していることをchangeメソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。

ロールバック後のschemaファイルを以下に示します。

db/schema.rb

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 0) do

end

usersテーブルのカラムに関する記述が消えているので、マイグレートする前の状態に戻ったようです。

3. もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。

コードは省略しますが、db/schema.rbの内容が元に戻ることを確認しました。

6.1.2 modelファイル

1. Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。

>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.class.superclass
=> ApplicationRecord(abstract)

2. 同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。

>> ApplicationRecord.superclass
=> ActiveRecord::Base

6.1.3 ユーザーオブジェクトを作成する

1. user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。

>> user.name.class=> String
>> user.email.class
=> String

user.nameとuser.emailがStringクラスのインスタンスであることを確認できました。

2. created_atとupdated_atは、どのクラスのインスタンスでしょうか?

>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

両者はActiveSupport::TimeWithZoneクラスのインスタンスです。

6.1.4 ユーザーオブジェクトを検索する

1. nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。

>> User.find_by(name: "Michael Hartl")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-07-13 17:54:18", updated_at: "2019-07-13 17:54:18">
>> User.find_by_name("Michael Hartl")
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-07-13 17:54:18", updated_at: "2019-07-13 17:54:18">

2. 実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。

>> User.all.class
=> User::ActiveRecord_Relation

3. User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)

>> User.all.length
  User Load (0.1ms)  SELECT "users".* FROM "users"
=> 1

6.1.5 ユーザーオブジェクトを更新する

1. userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。

=> "yamada"
>> user.save
   (0.2ms)  SAVEPOINT active_record_1
  User Update (0.5ms)  UPDATE "users" SET "name" = ?, "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "yamada"], ["email", "m@example.com"], ["updated_at", "2019-07-13 18:42:56.552791"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

2. 今度はupdate_attributesを使って、email属性を更新および保存してみてください。

>> user.update_attributes(name: "Yanagida", email: "y@gmail.com")
   (0.4ms)  SAVEPOINT active_record_1
  User Update (0.7ms)  UPDATE "users" SET "name" = ?, "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Yanagida"], ["email", "y@gmail.com"], ["updated_at", "2019-07-13 18:44:59.680964"], ["id", 1]]
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> true

3. 同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。

>> user.update_attributes(created_at: 1.year.ago)   (0.1ms)  SAVEPOINT active_record_1
  User Update (0.3ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2018-07-13 18:46:23.868735"], ["updated_at", "2019-07-13 18:46:23.882587"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

6.2 ユーザーを検証する

6.2.1 有効性を検証する

1. コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。

>> user = User.new(name: "Ruby Rails")
=> #<User id: nil, name: "Ruby Rails", email: nil, created_at: nil, updated_at: nil>
>> user.valid?
=> true

2. 6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。

>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.valid?
=> true

6.2.2 存在性を検証する

1. 新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]

2. u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?

>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors.messages[:email]
=> ["can't be blank"]

6.2.3 長さを検証する

1. 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

>> user = User.new(name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@mail.net")
=> #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", created_at: nil, updated_at: nil>
>> user.valid?
=> false

2. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

>> user.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]

6.2.4 フォーマットを検証する

1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

Your regular expression:

[\w+\-.]+@[a-z\d\-.]+\.[a-z]+

Your test string:

user@example.com
USER@foo.COM
A_US-ER@foo.bar.org
first.last@foo.jp
alice+bob@baz.cn

user@example,com
user_at_foo.org
user.name@example.
foo@bar_baz.com
foo@bar+baz.com

Match resultに上から5番目までのメールアドレスの文字背景に色がついていればOKです。

https://rubular.com/r/sAo4p5M1E6bg3R

2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。

テストにfoo@bar..comを追加します。

test/models/user_test.rb

test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com foo@bar..com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
    end

テストを実行します。

$ rails test:models
Started with run options --seed 32530

 FAIL["test_email_validation_should_reject_invalid_addresses", #<Minitest::Reporters::Suite:0x00007fa06b19bc08 @name="UserTest">, 0.03620800003409386]
 test_email_validation_should_reject_invalid_addresses#UserTest (0.04s)
        "foo@bar..com" should be invalid
        test/models/user_test.rb:46:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:44:in `each'
        test/models/user_test.rb:44:in `block in <class:UserTest>'

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

Finished in 0.03694s
7 tests, 16 assertions, 1 failures, 0 errors, 0 skips

結果はredです。リスト6.23よりuser.rbで使用する正規表現VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/iとして再度テストします。

$ rails test:models
Started with run options --seed 30297

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

Finished in 0.02860s
7 tests, 16 assertions, 0 failures, 0 errors, 0 skips

greenになることが確認できました。

3. foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

確認できました。

https://rubular.com/r/6UlnUeFkdbBL3X

6.2.5 一意性を検証する

1. リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。

メールアドレスを小文字にするテストをリスト 6.32に追加し、user.rbのbefore_saveの行をコメントアウトしてテストを実行します。

$ rails test:models
Started with run options --seed 55447

 FAIL["test_email_addresses_should_be_saved_as_lower-case", #<Minitest::Reporters::Suite:0x00007f935f3febd0 @name="UserTest">, 0.04487500002142042]
 test_email_addresses_should_be_saved_as_lower-case#UserTest (0.04s)
        Expected: "foo@example.com"
          Actual: "Foo@ExAMPle.CoM"
        test/models/user_test.rb:61:in `block in <class:UserTest>'

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

Finished in 0.05882s
9 tests, 17 assertions, 1 failures, 0 errors, 0 skips

redになることが確認できました。コメントアウトを解除して再度テストします。

$ rails test:models
Started with run options --seed 55480

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

Finished in 0.06251s
9 tests, 17 assertions, 0 failures, 0 errors, 0 skips

greenになることが確認できました。

2. テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。

app/models/user.rb

class User < ApplicationRecord
    before_save { email.downcase! }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

テストを実行します。

$ rails test:models
Started with run options --seed 3506

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

Finished in 0.07236s
9 tests, 17 assertions, 0 failures, 0 errors, 0 skips

greenになることが確認できました。

6.3 セキュアなパスワードを追加する

6.3.1 ハッシュ化されたパスワード

演習なし。

6.3.2 ユーザーがセキュアなパスワードを持っている

1. この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。

>> user = User.new(name: "foo", email: "foo@example.com")=> #<User id: nil, name: "foo", email: "foo@example.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "foo@example.com"], ["LIMIT", 1]]
=> false

2. なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。

>> user.errors.full_messages
=> ["Password can't be blank"]

パスワードが設定されていないから。

6.3.3 パスワードの最小文字数

1. 有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。

>> user = User.new(name: "foo", email: "foo@example.com", password: "abc", password_confirmation: "abc")
=> #<User id: nil, name: "foo", email: "foo@example.com", created_at: nil, updated_at: nil, password_digest: "$2a$12$xBDNuHJPMcihRHSEP2vsCeMTU8gV8Nu2q5ofQ0xsMVO...">
>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "foo@example.com"], ["LIMIT", 1]]
=> false

2. 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。

>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]

6.3.4 ユーザーの作成と認証

1. コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。

>> user = User.find_by(name: "Michael Hartl")
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["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...">

2. オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?

>> user.name = "Mike Hardin"
=> "Mike Hardin"
>> 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

保存に失敗してしまいます。 userオブジェクトのエラー内容を確認してみます。

>> user.errors.full_messages
=> ["Password can't be blank", "Password is too short (minimum is 6 characters)"]

userにパスワードが設定されていないため、バリデーションで弾かれています。 ※「User.createメソッドを使ってユーザーを作成したときにパスワードを設定したじゃないか」と思う人もいるかもしれませんが、パスワードはハッシュ化されpassword_digest属性に保存されるため、データベースからユーザーデータを取得しても直接パスワードが得られるわけではありません。今回の場合、userオブジェクトのpassword属性とpassword_confirmation属性は空っぽの状態です。

3. 今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。

update_attributeメソッドを用いて指定の属性を更新します。

>> user.update_attribute(:name, "Mike Hardin")
   (0.1ms)  SAVEPOINT active_record_1
  User Update (0.7ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Mike Hardin"], ["updated_at", "2019-07-28 15:53:42.066320"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

無事更新することができました。

6.4 最後に

6.4.1 本章のまとめ

演習なし。

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

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

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

  • 5.1 構造を追加する
    • 5.1.1 ナビゲーション
      • 1. Webページと言ったらネコ画像、というぐらいにはWebにはネコ画像が溢れていますよね。リスト 5.4のコマンドを使って、図 5.3のネコ画像をダウンロードしてきましょう。
      • 2. mvコマンドを使って、ダウンロードしたkitten.jpgファイルを適切なアセットディレクトリに移動してください (参考: 5.2.1)。
      • 3. image_tagを使って、kitten.jpg画像を表示してみてください (図 5.4)。
    • 5.1.2 BootstrapとカスタムCSS
      • 1. リスト 5.10を参考にして、5.1.1.1で使ったネコ画像をコメントアウトしてみてください。また、ブラウザのHTMLインスペクタ機能を使って、コメントアウトするとHTMLのソースからも消えていることを確認してみてください。
      • 2. リスト 5.11のコードをcustom.scssに追加し、すべての画像を非表示にしてみてください。うまくいけば、Railsのロゴ画像がHomeページから消えるはずです。先ほどと同様にインスペクタ機能を使って、今度はHTMLのソースコードは残ったままで、画像だけが表示されなくなっていることを確認してみてください。
    • 5.1.3 パーシャル (partial)
      • 1. Railsがデフォルトで生成するheadタグの部分を、リスト 5.18のようにrenderに置き換えてみてください。ヒント: 単純に削除してしまうと後でパーシャルを1から書き直す必要が出てくるので、削除する前にどこかに退避しておきましょう。
      • 2. リスト 5.18のようなパーシャルはまだ作っていないので、現時点ではテストはredになっているはずです。実際にテストを実行して確認してみましょう。
      • 3. layoutsディレクトリにheadタグ用のパーシャルを作成し、先ほど退避しておいたコードを書き込み、最後にテストが green に戻ることを確認しましょう。
  • 5.2 Sassとアセットパイプライン
    • 5.2.1 アセットパイプライン
    • 5.2.2 素晴らしい構文を備えたスタイルシート
      • 1. 5.2.2で提案したように、footerのCSSを手作業で変換してみましょう。具体的には、リスト 5.17の内容を1つずつ変換していき、リスト 5.20のようにしてみてください。
  • 5.3 レイアウトのリンク
    • 5.3.1 Contactページ
    • 5.3.2 RailsのルートURL
      • 1. 実は名前付きルートは、as:オプションを使って変更することができます。有名なFar Sideの漫画に倣って、Helpページの名前付きルートをhelfに変更してみてください (リスト 5.29)。
      • 2. 先ほどの変更により、テストが redになっていることを確認してください。リスト 5.28を参考にルーティングを更新して、テストを greenにして見てください。
      • 3. エディタのUndo機能を使って、今回の演習で行った変更を元に戻して見てください。
    • 5.3.3 名前付きルート
      • 1. リスト 5.29のようにhelfルーティングを作成し、レイアウトのリンクを更新してみてください。
      • 2. 前回の演習と同様に、エディタのUndo機能を使ってこの演習で行った変更を元に戻してみてください。
    • 5.3.4 リンクのテスト
      • 1. footerパーシャルのabout_pathをcontact_pathに変更してみて、テストが正しくエラーを捕まえてくれるかどうか確認してみてください。
      • 2. リスト 5.35で示すように、Applicationヘルパーで使っているfull_titleヘルパーを、test環境でも使えるようにすると便利です。こうしておくと、リスト 5.36のようなコードを使って、正しいタイトルをテストすることができます。ただし、これは完璧なテストではありません。例えばベースタイトルに「Ruby on Rails Tutoial」といった誤字があったとしても、このテストでは発見することができないでしょう。この問題を解決するためには、full_titleヘルパーに対するテストを書く必要があります。そこで、Applicationヘルパーをテストするファイルを作成し、リスト 5.37のFILL_INの部分を適切なコードに置き換えてみてください。ヒント: リスト 5.37ではassert_equal <期待される値>, <実際の値>といった形で使っていましたが、内部では==演算子で期待される値と実際の値を比較し、正しいかどうかのテストをしています。
  • 5.4 ユーザー登録: 最初のステップ
    • 5.4.1 Usersコントローラ
      • 1. 表 5.1を参考にしながらリスト 5.41を変更し、users_new_urlではなくsignup_pathを使えるようにしてみてください
      • 2. 先ほどの変更を加えたことにより、テストが redになったことを確認してください。なお、この演習はテスト駆動開発 (コラム 3.3) で説明した red/green のリズムを作ることを目的としています。このテストは次の5.4.2で greenになるよう修正します。
    • 5.4.2
      • 1. もしまだ5.4.1.1の演習に取り掛かっていなければ、まずはリスト 5.41のように変更し、名前付きルートsignup_pathを使えるようにしてください。また、リスト 5.43で名前付きルートが使えるようになったので、現時点でテストが greenになっていることを確認してください。
      • 2. 先ほどのテストが正しく動いていることを確認するため、signupルートの部分をコメントアウトし、テストredになることを確認してください。確認できたら、コメントアウトを解除してgreenの状態に戻してください。
      • 3. リスト 5.32の統合テストにsignupページにアクセスするコードを追加してください (getメソッドを使います)。コードを追加したら実際にテストを実行し、結果が正しいことを確認してください。ヒント: リスト 5.36で紹介したfull_titleヘルパーを使ってみてください。
  • 5.5 最後に
    • 5.5.1 本章のまとめ
続きを読む