T_Y_CODE

プログラミング学習記録

学習記録 16日目

16日目の学習記録をまとめていきます。

学習計画

学習内容

Ruby on Railsチュートリアル 6章

6.1 Userモデル
  • トピックブランチを作成します。
$ git checkout -b modeling-users
  • rails generate modelコマンドを使用してUserモデルを作成します。Userモデルにはname, emailの2つのカラムがあります。どちらのカラムもデータ型はstringにします。
$ rails generate model User name:string email:string
Running via Spring preloader in process 73153
      invoke  active_record
      create    db/migrate/20201021023859_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
# db/migrate/20201021023859_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
  • create_tableメソッドはブロックを受け取りブロック内で各カラムを作成します。
  • t.timestampsは特別なコマンドでcreated_atとupdated_atという2つのマジックカラムを作成します。
  • マイグレーションの適用は以下のコマンドを使用します。
$ rails db:migrate
  • マイグレーションを適用するとdb/development.sqlite3というファイルが作成されます。DB Browser for SQLiteというソフトを使ってデータベースの構造を確認してみます。

f:id:t_y_code:20201021115239p:plain

演習

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

# db/migrate/20201021023859_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
# db/schema.rb
ActiveRecord::Schema.define(version: 2020_10_21_023859) do

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

end
  • t.timestampがcreated_atとupdated_atに変わっている以外同じ内容になっている。

2. ほぼすべてのマイグレーションは、元に戻すことが可能です(少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック(rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください。(以下略)

  • rollback実行してみます。
$ rails db:rollback
  • schema.rbの内容を見てみます。
# db/schema.rb
ActiveRecord::Schema.define(version: 2020_10_21_023859) do

end
  • db:migrateする前の状態に戻っています。

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

$ rails db:migrate
  • schema.rbの内容が元に戻りました。
# db/schema.rb
ActiveRecord::Schema.define(version: 2020_10_21_023859) do

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

end
6.1.2 modelファイル
演習

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

  • railsコンソールを使って調べていきます。
$ rails c
> user = User.new
> user
# => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
> user.class.superclass
# => ApplicationRecord(abstract)
> user.class.superclass.superclass
# => ActiveRecord::Base
6.1.3 ユーザーオブジェクト
  • Railsコンソール上でデータベースを更新させないために--sandboxオプションを使用してコンソールを起動する。
  • モデルのインスタンスが登録できる状態かどうかはvalid?メソッドで確認出来る。
  • モデルのインスタンスをデータベースに登録するにはsaveメソッドを使用します。登録に成功すればtrueを返します。
$ rails c --sandbox
> user = User.new(name: "Michael Hartl", email: "michael@example.com")
> user.valid?
# => true
> user.save
(0.1ms)  SAVEPOINT active_record_1
  User Create (0.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Michael Hartl"], ["email", "michael@example.com"], ["created_at", "2020-10-21 03:06:47.159996"], ["updated_at", "2020-10-21 03:06:47.159996"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
  • モデルのインスタンスはドット記法を用いて属性にアクセスできる。
> user.name
# => "Michael Hartl"
> user.email
# => "michael@example.com"
> user.updated_at
# => Wed, 21 Oct 2020 03:06:47 UTC +00:00
  • モデルの生成と保存を一気に行うにはcreateメソッドを使用します。
> User.create(name: "A Nother", email: "another@example.org")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.1ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "A Nother"], ["email", "another@example.org"], ["created_at", "2020-10-21 03:11:03.433361"], ["updated_at", "2020-10-21 03:11:03.433361"]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
# => #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2020-10-21 03:11:03", updated_at: "2020-10-21 03:11:03">
> foo = User.create(name: "Foo", email: "foo@bar.com")
   (0.1ms)  SAVEPOINT active_record_1
  User Create (0.1ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Foo"], ["email", "foo@bar.com"], ["created_at", "2020-10-21 03:11:12.465011"], ["updated_at", "2020-10-21 03:11:12.465011"]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
> foo
# => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2020-10-21 03:11:12", updated_at: "2020-10-21 03:11:12">
  • destroyメソッドを使用するとデータベース上からデータを削除出来ます。データベース上から削除なのでインスタンスにはデータは入ったままになります。
> foo.destroy
   (0.1ms)  SAVEPOINT active_record_1
  User Destroy (0.1ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 3]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
# => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2020-10-21 03:11:12", updated_at: "2020-10-21 03:11:12">
> foo
# => #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2020-10-21 03:11:12", updated_at: "2020-10-21 03:11:12">
演習

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

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

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

> user.created_at.class
# => ActiveSupport::TimeWithZone
> user.updated_at.class
# => ActiveSupport::TimeWithZone
6.1.4 ユーザーオブジェクトを検索する
  • モデルのデータベース上の登録情報を確認するにはfindメソッドを使用する。id番号で検索できる。id=3は先ほどdestroyしたので検索に引っかからない。
> User.find(1)
# => #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 03:06:47", updated_at: "2020-10-21 03:06:47">
> User.find(3)
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=3)
  • 他の検索方法としてfind_byメソッドがある。カラム名とデータ内容で検索できる。
> User.find_by(email: "michael@example.com")
# => #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 03:06:47", updated_at: "2020-10-21 03:06:47">
  • 先頭の行や末尾の行を検索するにはfirst, lastメソッドを使用する。
> User.first
# => #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 03:06:47", updated_at: "2020-10-21 03:06:47">
> User.last
# => #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2020-10-21 03:11:03", updated_at: "2020-10-21 03:11:03">
  • 全てのデータを出力するにはallメソッドを使用します。
> User.all
# => #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 03:06:47", updated_at: "2020-10-21 03:06:47">, #<User id: 2, name: "A Nother", email: "another@example.org", created_at: "2020-10-21 03:11:03", updated_at: "2020-10-21 03:11:03">]>
演習

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

> User.find_by_name("Michael Hartl")
# => #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 03:06:47", updated_at: "2020-10-21 03:06:47">

2. 実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。
3. User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください(4.2.2)。

> User.all.class
# => User::ActiveRecord_Relation
> User.all.length
# => 2
6.1.5 ユーザーオブジェクトを更新する
  • ユーザーオブジェクトはsaveメソッドを使用して簡単に更新出来る。また、save前にreloadを使用するとデータベース上から元のデータを持ってきてくれる。
> user
# => #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 03:06:47", updated_at: "2020-10-21 03:06:47">
> user.email
# => "michael@example.com"
> user.email = "mhartl@example.net"
> user.save
# => true
> user.email = "foo@bar.com"
> user.reload.email
> user.email
# => "mhartl@example.net"
  • また、updateメソッドを使用することでsaveまで一気に行えます。正しupdateメソッドはカラムの保存が1つでも失敗すると全て失敗するようになっています。
> user.update(name: "The Dude", email: "dude@abides.org")
# => true
> user.name
# => "The Dude"
> user.email
# => "dude@abides.org"
  • 特定の属性のみ更新したい場合はupdate_attributeメソッドを使用する。
> user.update_attribute(:name, "El Duderio")
# => true
> user.name
# => "El Duderino"
演習

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

> user = User.create(name: "Foo Bar", email: "foo@bar.com")
# => true
> user
# => #<User id: 1, name: "Foo Bar", email: "foo@bar.com", created_at: "2020-10-21 03:43:27", updated_at: "2020-10-21 03:43:27">
> user.name = "ChangedName"
> user.save
> User.first
# => #<User id: 1, name: "ChangedName", email: "foo@bar.com", created_at: "2020-10-21 03:43:27", updated_at: "2020-10-21 03:44:38">

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

> user.update_attribute(:email, "updated@example.com")
# => true
> User.first
# => #<User id: 1, name: "ChangedName", email: "updated@example.com", created_at: "2020-10-21 03:43:27", updated_at: "2020-10-21 03:46:23">

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

> user.update_attribute(:created_at, 1.year.ago)
# => true
> User.first
# => #<User id: 1, name: "ChangedName", email: "updated@example.com", created_at: "2019-10-21 03:47:49", updated_at: "2020-10-21 03:47:49">
6.2.1 有効性を検証する
  • モデルのテストを行います。Userモデルのインスタンスを1つ作り有効なオブジェクトかをテストします。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end
end
  • テストが成功することを確認します。モデルのテストのみ行うには以下のコマンドを使用する。
$ rails test:models
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
演習

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

$ rails c --sandbox
> user = User.new(name: "Example User", email: "user@example.com")
> user.valid?
# => true

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

> user = User.new(name: "Michael Hartl", email: "michael@example.com")
> user.valid?
# => true
6.2.2 存在性を検証する
  • テストにnameが空白だった際、valid?がfalseになるかの検証を記載する。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "name should be present" do
    @user.name = "    "
    assert_not @user.valid?
  end
end
  • テストが失敗します。つまりnameが空白でもvalid?がtrueになってしまっています。
$ rails test:models
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips
  • nameに必ず文字列が入っていけないと検査する方法はモデルにvalidatesを追記します。presenceをtrueにすることで空白文字の場合valid?がfalseになります。
# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
end
  • railsコンソールで正しく検査されているか確認します。
$ rails c
> user = User.new(name: "", email: "michael@example.com")
> user.valid?
# => false
> user.errors.full_messages
# => ["Name can't be blank"]
> user.save
# => false
  • 以上によりテストが成功します。
$ rails test:models
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
  • emailも同様にpresence設定します。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email should be present" do
    @user.email = "      "
    assert_not @user.valid?
  end
end
$ rails test:models
3 tests, 3 assertions, 1 failures, 0 errors, 0 skips
# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
end
$ rails t
10 tests, 22 assertions, 0 failures, 0 errors, 0 skips
演習

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

$ rails c --sandbox
> u = User.new
> 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 長さを検証する
  • nameの長さの上限を50文字, emailは255文字に制限する。まずはテストを書く。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "name should not bee too long" do
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not bee too long" do
    @user.email = "a" * 244 + "@examole.com"
    assert_not @user.valid?
  end
end
  • テストが失敗することを確認します。
$ rails test:models
5 tests, 5 assertions, 2 failures, 0 errors, 0 skips
  • モデルファイルにvalidatesを追記していく。文字数の上限はlength: { maximum: }を使用する。
# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end
  • テストが成功することを確認します。
$ rails test:models
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips
演習

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

$ rails c --sandbox
> user = User.new(name: "a" * 51, email: "a" * 244 + "@example.com")
> 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 フォーマットを検証する
  • メールがちゃんとしたメールのフォーマットをしているか検証していきます。まずはテストを書きます。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end
  • まだemailのフォーマットを指定していないのでテストは成功します。
$ rails test:models
6 tests, 10 assertions, 0 failures, 0 errors, 0 skips
  • 配列内のメールアドレスを正しくないフォーマットに変更しassert_notでテストします。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert_not @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end
$ rails test:models
6 tests, 6 assertions, 1 failures, 0 errors, 0 skips
  • 正規表現を用いてemailのフォーマットを指定します。
# app/models/user.rb
class User < ApplicationRecord
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
end
  • フォーマットの指定をしたのでテストが成功します。
$ rails test:models
6 tests, 10 assertions, 0 failures, 0 errors, 0 skips
演習

1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
f:id:t_y_code:20201021134200p:plain
2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。

# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com foo@bar..com]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert_not @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end
$ rails test:models
6 tests, 11 assertions, 1 failures, 0 errors, 0 skips

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

# app/models/user.rb
class User < ApplicationRecord
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
end
$ rails test:models
6 tests, 11 assertions, 0 failures, 0 errors, 0 skips
6.2.5 一意性を検証する
  • メールアドレスは一意でなければなりません。varidatesメソッドの:uniquenessオプションを使用することで一意性を強制することが出来ます。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email address should be unique" do
    duplicate_user =  @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
end
$ rails test:models
7 tests, 12 assertions, 1 failures, 0 errors, 0 skips
# app/models/user.rb
class User < ApplicationRecord
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
end
$ rails test:models
7 tests, 12 assertions, 0 failures, 0 errors, 0 skips
  • 現状はFOO@BAR.COMとfoo@bar.comは別々のアドレスとみなされています。一般的にメールアドレスは大文字と小文字は区別しません。テストを修正します。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email address should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end
end
$ rails test:models
7 tests, 12 assertions, 1 failures, 0 errors, 0 skips
  • :uniquenessオプションには:case_sensitiveオプションがありこのオプションをfalseにすることで大文字と小文字を区別しなくなります。
# app/models/user.rb
class User < ApplicationRecord
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }
end
  • これでテストは成功するようになります。
$ rails test:models
7 tests, 12 assertions, 0 failures, 0 errors, 0 skips
  • またデータベースにindexを追加して一意性をデータベースの登録時にも行うようにします。
$ rails g migration add_index_to_users_email
  • マイグレーションに以下の記述を追記します。add_indexメソッドで指定のカラムにインデックスを追加しuniqueオプションをtrueにすることで一意性を強制できます。
# db/migrate/20201021050120_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
  end
end
  • データベースをマイグレートします。
$ rails db:migrate
  • テスト用のfixtureを空にしておく。(一意でないため)
  • データベース上ではまだ大文字と小文字を区別していない。この問題を解決する方法としてデータベースに保存する前にメールアドレスの文字列を小文字にしてしまうことである。
# app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
end
  • テストが成功することを確認します。
$ rails test:models
7 tests, 12 assertions, 0 failures, 0 errors, 0 skips
演習

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

# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

 #...

  test "email address should be saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    assert_equal mixed_case_email.downcase, @user.reload.email
  end
end
$ rails test:models
8 tests, 13 assertions, 0 failures, 0 errors, 0 skips

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

# app/models/user.rb
class User < ApplicationRecord
  before_save { email.downcase! }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
end
$ rails test:models
8 tests, 13 assertions, 0 failures, 0 errors, 0 skips
6.3.1 ハッシュ化されたパスワード
  • セキュアにハッシュ化されたパスワードをデータベースのpassword_digestに保存できるようにする。XXX_digestという属性を作成するとXXX, XXX_confirmationという2つの仮想的な属性を使用出来るようになる。またauthenticateメソッドが使用出来るようになる。これは引数がパスワードと一致していればオブジェクトを返すメソッド。
  • password_digestを追加するマイグレーションを作成する。
$ rails g migration add_password_digest_to_users password_digest:string
# db/migrate/20201021051857_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :password_digest, :string
  end
end
  • データベースをマイグレートします。
$ rails db:migrate
6.3.2 ユーザーがセキュアなパスワードを持っている
  • Userモデルでhas_secure_passwordを使用します。これでpassword属性とpassword_confirmation属性が使用出来るようになりました。
# app/models/user.rb
class User < ApplicationRecord
  before_save { email.downcase! }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
  has_secure_password
end
  • テストを修正します。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar")
  end

 #...
end
  • テストが成功することを確認します。
$ rails test:models
8 tests, 13 assertions, 0 failures, 0 errors, 0 skips
$ rails t
15 tests, 32 assertions, 0 failures, 0 errors, 0 skips
演習

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

$ rails c --sandbox
> user = User.new(name: "Foo Bar", email: "foo@bar.com")
> user.valid?
# => false

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

> user.errors.full_messages
# => ["Password can't be blank"]
6.3.3 パスワードの最小文字数
  • パスワードの最小文字数を6文字に設定します。まずテストから書いていきます。
# test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar")
  end

 #...

  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end
- テストが失敗することを確認します。
>|command|
$ rails t
17 tests, 34 assertions, 2 failures, 0 errors, 0 skips
  • validatesメソッドでpasswordにpresence: trueとlength: { minimum: }を設定します。
# app/models/user.rb
class User < ApplicationRecord
  before_save { email.downcase! }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
end
  • テストが成功することを確認します。
$ rails t
17 tests, 34 assertions, 0 failures, 0 errors, 0 skips
演習

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

$ rails c --sandbox
> user = User.new(name: "Foo Bar", email: "foo@bar.com", password: "foo", password_confirmation: "foo")
> user.valid?
# => false

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

> user.errors.full_messages
# => ["Password is too short (minimum is 6 characters)"]
6.3.4 ユーザーの作成と認証
  • 次の章に向けてUserモデルに1つオブジェクトを追加しておきます。
$ rails c
> User.create(name: "Michael Hartl", email: "michael@example.com", password: "foobar", password_confirmation: "foobar")
  • うまく作成されたかdb/development.sqlite3の中身を確認します。

f:id:t_y_code:20201021144220p:plain

演習

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

$ rails c
> user = User.first
> user
# => #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2020-10-21 05:40:52", updated_at: "2020-10-21 05:40:52", password_digest: [FILTERED]>

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

  • passwrod, password_confirmationがnilのため
$ rails c
> user.name = "foobar"
> user.save
# => false
> user.errors.full_messages
# => ["Password can't be blank", "Password is too short (minimum is 6 characters)"]
> user.password
# => nil
> user.password_confirmation
# => nil

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

> user.update_attribute(:name, "Foo Bar")
# => true
> user.name
# => "Foo Bar"
6.4 最後に
  • リモートリポジトリへプッシュしてherokuへデプロイします。
$ rails t
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"
$ git push origin modering-users
$ git checkout master
$ git merge modering-users
$ git push origin master
$ git push heroku
$ heroku run rails db:migrate
  • 6章は以上で終了です。

Ruby on Railsチュートリアル 7章

7.1 ユーザーを表示する
  • トピックブランチを作成します。
$ git checkout -b sign-up
7.1.1 デバックとRails環境
  • アプリケーションにデバッグ情報を出力するようにします。
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= render 'layouts/rails_default'%>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header'%>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
</html>
  • custom.scssにてデバッグ情報のレイアウトに関する追記を行います。
  • Railsサーバを立ち上げ動作確認をします。

f:id:t_y_code:20201021193809p:plain

演習

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

  • static_pagesコントローラのaboutアクションが使用されています。

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

  • 同じ内容が表示される。
$ rails c --sandbox
> user = User.first
> puts user.attributes.to_yaml
---
id: 1
name: Michael Hartl
email: michael@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2020-10-21 05:40:52.899711000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2020-10-21 05:48:52.973765000 Z
  zone: *2
  time: *3
password_digest: "$2a$12$UUe6iuNT8ToJvzjx/GfItuQIu3aXoyXFDPF0GBP/bFrZU.438WEvO"
> y user.attributes
---
id: 1
name: Michael Hartl
email: michael@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2020-10-21 05:40:52.899711000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2020-10-21 05:48:52.973765000 Z
  zone: *2
  time: *3
password_digest: "$2a$12$UUe6iuNT8ToJvzjx/GfItuQIu3aXoyXFDPF0GBP/bFrZU.438WEvO"
7.1.2 Userリソース
  • UserをRESTfulなアクションを有効にするためにルーティングを追加します。
# config/routes.rb
Rails.application.routes.draw do
  root "static_pages#home"
  get '/help',    to: 'static_pages#help'
  get '/about',   to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
  get '/signup',  to: 'users#new'
  resources :users
end
  • まずは/users/:idのGETリクエストのusers#showを作成していきます。まずはビューから作成。
$ touch app/views/users/show.html.erb
# app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>
  • アクションも作成します。ビュー内で使用するローカル変数@userを定義します。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
  end

  def show
    @user = User.find(params[:id])
  end
end
  • Railsサーバで動作確認を行います。

f:id:t_y_code:20201021195149p:plain

演習

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

  • ビューを編集します。
# app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>

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

# app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>, <%= @user.created_at %>, <%= @user.updated_at %><br>
<%= Time.now %>

f:id:t_y_code:20201021195417p:plain

7.1.3 debuggerメソッド
  • byebug gemによりdebuggerメソッドが使用できる。このメソッドを実行するとアプリケーションの動作を一時停止しターミナル上でデバッグを行うことが出来る。(変数の値を確認したり)
演習

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

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
  end

  def show
    @user = User.find(params[:id])
    debugger # <- この位置で停止しデバッグが出来る
  end
end
(byebug) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false

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

  • newアクション内では@userは定義されていないためnilとなる。
(byebug) @user
nil
7.1.4 Gravatar画像とサイドバー
  • gravatar_forヘルパーメソッドを使用してGravatarの画像を利用出来るようにします。
# app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1>
  <%= gravatar_for @user %>
  <%= @user.name %>
</h1>
  • gravatar_forヘルパーメソッドを定義します。GravatarのURLはメールアドレスをMD5でハッシュ化したものが使用される。MD5のハッシュ化はDigest::MD5::hexdigestメソッドを使用する。
# app/helpers/users_helper.rb
module UsersHelper
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end
  • 画像が表示されました。

f:id:t_y_code:20201021200849p:plain

  • railsコンソールを使用してデータベースの内容を変更します。
$ rails c
> user = User.first
> user.update(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar")
# => true
  • 情報、画像が変更されたことを確認します。

f:id:t_y_code:20201021201143p:plain

演習

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

  • 省略

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

# app/helpers/users_helper.rb
module UsersHelper
  def gravatar_for(user, options = { size: 80 })
    size = options[:size]
    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

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

  • 演習2はハッシュのキーにsizeという名前を付けて値を渡している。演習3はメソッドのキーワード引数に値を渡している。
# app/helpers/users_helper.rb
module UsersHelper
  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.1 form_withを使用する
  • form_withヘルパーメソッドを使用することでActiveRecordのオブジェクトを取り込みそのオブジェクトの属性を使ってフォームを構築出来ます。
  • まずはローカル変数@userを定義します。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def show
    @user = User.find(params[:id])
  end
end
  • ビューにフォームを追記します。
# app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>
      
      <%= f.label :email %>
      <%= f.email_field :email %>
      
      <%= f.label :password %>
      <%= f.password_field :password %>
      
      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>
      
      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
  • app/assets/stylesheets/custom.scssを編集してフォームの見た目を整えます。
  • 動作確認をします。

f:id:t_y_code:20201021202710p:plain

演習

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

  • foobarへ変更しても動作することを確認。変数名fはform_withのfでありfoobarという変数名は適当ではない。
7.2.2 フォームHTML
  • Userモデルが新規作成される時はusersコントローラのcreateアクションが実行されます。createアクションにUserモデルを新規作成するコードを記載していきます。
# app/controllers/users_controller.rb
class UsersController < ApplicationController

  #...

  def create
    @user = User.new(params[:user])
    if @user.save

    else
      render 'new'
    end
  end
end
  • 実際にcreateアクションを実行するとエラーになります。

f:id:t_y_code:20201021204254p:plain

  • このエラーはRails4.0以降ではStrong Parametersという手法を使用することが標準になったためです。
7.3.2 Strong Parameters
  • Strong Parametersを使用すると以下のようなコードになります。
# app/controllers/users_controller.rb
class UsersController < ApplicationController

  #...

  def create
    @user = User.new(user_params)
    if @user.save

    else
      render 'new'
    end
  end

  private
    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end
end
7.3.3 エラーメッセージ
  • ユーザー登録失敗時にエラーメッセージが表示されるようビューを変更します。
# app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>
      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>
      
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      
      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>
      
      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>
      
      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
  • shared/error_messagesパーシャルを作成します。Railsの慣習として、複数のビューで使用されるパーシャルはsharedディレクトリによく置かれます。
$ mkdir app/views/shared
$ touch app/views/shared/_error_messages.html.erb
# app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>
    </div>
    <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>
  • CSSを整えて動作確認をします。

f:id:t_y_code:20201021205907p:plain

演習

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

  • バリデーションを変更します。
# app/models/user.rb
class User < ApplicationRecord
  before_save { email.downcase! }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 5 }
end
  • エラーメッセージも変更されました。

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

# 未送信のユーザー登録フォームURL
# ルーティングでいうget '/signup',  to: 'users#new'が実行されている。
http://127.0.0.1:3000/signup
# 送信済みのユーザー登録フォームURL
# ルーティングでいうresources :usersが実行されている。
http://127.0.0.1:3000/users
7.3.4 失敗時のテスト
  • ユーザ新規登録用の統合テストを作成します。
$ rails g integration_test users_signup
Running via Spring preloader in process 2573
      invoke  test_unit
      create    test/integration/users_signup_test.rb
  • テストを書いていきます。無効なユーザ登録のPOSTを送った前後でUser.countに差異がないかをチェックしています。
# 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 users_path, params: { user: {  name: "",
                                          email: "user@invalid",
                                          password: "foo",
                                          password_confirmation: "bar" }}
    end
    assert_template 'users/new'
  end
end
  • テストが成功することを確認します。
$ rails test:integration
2 tests, 10 assertions, 0 failures, 0 errors, 0 skips
演習

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

# test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  test "invalid signup information" do
    #...
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.alert-danger'
  end
end
$ rails t
18 tests, 38 assertions, 0 failures, 0 errors, 0 skips
7.4.1 登録フォームの完成
  • 登録が成功した際、ユーザーページにリダイレクトするようにする。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  #...

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
      render 'new'
    end
  end

  #...
end
演習

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

$ rails c
> User.find(2)
# => #<User id: 2, name: "foobar", email: "foo@bar.com", created_at: "2020-10-21 12:22:38", updated_at: "2020-10-21 12:22:38", password_digest: [FILTERED]>
7.4.2 flash
  • flash変数に代入したメッセージはリダイレクトした直後のページで表示されるようになる。
# app/controllers/user_controller.rb
class UsersController < ApplicationController
  #...

  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  #...
end
  • flash変数の内容をWebサイトのレイアウトに追加します。
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= render 'layouts/rails_default'%>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header'%>
    <div class="container">
      <% flash.each do |message_type, message| %>
        <div class="alert alert-<%= message_type%>%"><%= message %></div>
      <% end %>
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
</html>
演習

1. コンソールに移り、文字列内の式展開(4.2.1)でシンボルを呼び出してみましょう。例えば"#{:success}"といったコードを実行すると、どんな値が返ってきますか? 確認してみてください。

$ rails c
> puts "#{:success}"
success

2. 先ほどの演習で試した結果を参考に、リスト 7.28のflashはどのような結果になるか考えてみてください。

> puts "#{flash[:success]}"
It worked!
> puts "#{flash[:danger]}"
It failed.
7.4.3 実際のユーザー登録
  • データベースのリセットをして動作確認をします。
$ rails db:migrate:reset

f:id:t_y_code:20201021231109p:plain

演習

1. Railsコンソールを使って、新しいユーザーが本当に作成されたのかもう一度チェックしてみましょう。結果は、リスト 7.30のようになるはずです。

$ rails c
> User.find_by(email: "example@railstutorial.org")
# => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2020-10-21 14:10:26", updated_at: "2020-10-21 14:10:26", password_digest: [FILTERED]>

2. 自分のメールアドレスでユーザー登録を試してみましょう。既にGravatarに登録している場合、適切な画像が表示されているか確認してみてください。

  • 省略
7.4.4 成功時のテスト
  • 有効な送信に対するテストを書きます。follow_redirect!メソッドを実行することでリダイレクト先に移動出来ます。
# test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  #...

  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: "foobar",
                                          password_confirmation: "foobar" }}
    end
    follow_redirect!
    assert_template 'users/show'
  end
end
  • テストが成功することを確認します。
$ rails t
19 tests, 40 assertions, 0 failures, 0 errors, 0 skips
演習

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

# test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  #...

  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: "foobar",
                                          password_confirmation: "foobar" }}
    end
    follow_redirect!
    assert_template 'users/show'
    assert_not flash.empty?
  end
end
$ rails t
19 tests, 41 assertions, 0 failures, 0 errors, 0 skips

2. 本文中でも指摘しましたが、flash用のHTML(リスト 7.29)は読みにくいです。より読みやすくしたリスト 7.33のコードに変更してみましょう。変更が終わったらテストスイートを実行し、正常に動作することを確認してください。なお、このコードでは、Railsのcontent_tagというヘルパーを使っています。

  • 指定の記述へ変更を行いテストが成功することを確認します。

3. リスト 7.26のリダイレクトの行をコメントアウトすると、テストが失敗することを確認してみましょう。

  • 省略

4. リスト 7.26で、@user.saveの部分をfalseに置き換えたとしましょう(バグを埋め込んでしまったと仮定してください)。このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか? テストコードを追って考えてみてください。

  • 有効な送信をしているのにUser.countの変化量が0(変化していない)ためエラーになる。
7.5 プロのデプロイ
  • 現在までの変更をローカルリポジトリにプッシュします。
$ rails t
$ git add -A
$ git commit -m "Finish user signup"
$ git push origin sign-up
  • トピックブランチをmasterブランチへマージします。
$ git checkout master
$ git merge sign-up
7.5.1 本番環境でのSSL
7.5.2 本番環境用のWebサーバー
  • 本番環境のWebサーバ設定を行います。config/puma.rbに本番環境の設定を記載します。設定内容はherokuの公式ドキュメントを参考にします。

Deploying Rails Applications with the Puma Web Server | Heroku Dev Center

  • HerokuでPumaを走らせるための設定ファイルを作成します。
7.5.3 本番データベースを設定する
  • herokuの公式ドキュメントを参考にconfig/database.ymlを編集します。

Getting Started on Heroku with Rails 6.x | Heroku Dev Center

7.5.4 本番環境へのデプロイ
  • リモートリポジトリへのプッシュおよびherokuへデプロイを行います。
$ rails t
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push origin master
$ git push heroku
演習

1. ブラウザから本番環境(Heroku)にアクセスし、SSLの鍵マークがかかっているか、URLがhttpsになっているかどうかを確認してみましょう。

  • 本番環境で確認します。ちゃんとなっていますね。

f:id:t_y_code:20201021234124p:plain
2. 本番環境でユーザーを作成してみましょう。Gravatarの画像は正しく表示されているでしょうか?

  • 正しく動作していますね。

f:id:t_y_code:20201021234231p:plain

  • 以上で7章は終了です。

本日の総括

  • モデルの作成, 検査, フォームの作成について学習した。曖昧になっていたhas_secure_passwordやform_withの知識を固められた。