0
0

Rails 7 でTODOアプリ ⑧ (バリデーションメッセージのテンプレート化)

Last updated at Posted at 2023-11-18

はじめに

前回は

Deviseのビューをカスタム・フラッシュメッセージ として

  • Deviseのビュー
  • Bootstrapの適用
  • フラッシュメッセージ

について学びました。

今回は

バリデーションメッセージのテンプレート化 として

  • Railsコンソール
  • バリデーション
  • errors
  • バリデーションメッセージ

について学びます。

では、はじめていきましょう。

1. コンソールを使ってみる

最初にちょっと脱線します。

rails c

ロジックの検証に是非とも使いたいのが、Railsコンソールです。

モデルのロジックやDBの接続も可能なので、データーの入力や削除にも使えますし、クエリの結果を調べたりもできます。

最近では使えるメソッドの候補等も表示されてとても親切なツールになりました。

では、早速ターミナルから作業を始めます。

一旦データーを初期状態に戻したいので、

todo_app % rails db:migrate:reset

でテーブルを初期化します。

 todo_app % rails db:migrate:reset 
Dropped database 'storage/development.sqlite3'
Dropped database 'storage/test.sqlite3'
Created database 'storage/development.sqlite3'
Created database 'storage/test.sqlite3'
== 20231105023538 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0008s
== 20231105023538 CreateTasks: migrated (0.0008s) =============================

== 20231108012222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0007s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0003s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0003s
== 20231108012222 DeviseCreateUsers: migrated (0.0013s) =======================

このようなログが流れたら次に、

todo_app % rails c

でコンソールを開きましょう。

 todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001> 

ここから、 データーを作成していきます。

new

Railsは、モデルとなるclass を使ってインスタンスを作成します。

新しいインスタンスなので、

irb(main):001> Task.new

このように書きます。

すると以下のように結果が返ってきます。

irb(main):001> Task.new
=> #<Task:0x0000000105dd8730 id: nil, title: nil, description: nil, start_time: nil, end_time: nil, status: "not_started", created_at: nil, updated_at: nil>

この時点では、enumを設定したデフォルト値のある status 以外の入力がありません。

続いて値を入力していきますが、方法は2通りあり

引数で渡す

irb(main):002> Task.new(title: "test")
=> 
#<Task:0x00000001088c4250
 id: nil,
 title: "test",
 description: nil,
 start_time: nil,
 end_time: nil,
 status: "not_started",
 created_at: nil,
 updated_at: nil>

一度変数に代入して後で足す

irb(main):003> task = Task.new(title: "test")
=>
#<Task:0x00000001088cb910
...
irb(main):004> task.description = "Hello World!"
=> "Hello World!"
irb(main):005> task
=>
#<Task:0x00000001088cb910
id: nil,
title: "test",
description: "Hello World!",
start_time: nil,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>

このようになります。ですので、インスタンスの作成後も値を入れ直す事が可能です。

モデルからインスタンスを作って値を代入する所まで来ましたが、このままではただのインスタンスですので、DBとの接続は行われていません。

save

インスタンスは、 save メソッドを実行する事で、DBの各テーブルにインスタンスの値をレコードします。

しかし、ここまでの状態のものを保存してみると、

irb(main):006> task.save
=> false

falseとだけ出て終わりました。これは、保存出来ていない事を示します。

ただ、これだけでは何があったか分からないですね。

その場合、

irb(main):007> task.save!
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/validations.rb:84:in `raise_validation_error': バリデー失敗しました: 開始日を入力してください (ActiveRecord::RecordInvalid)

このようなエラーとなります。また、エラーが出た原因として、 バリデーションメッセージが表示されています。

通常バリデーションに引っかかっても何も表示されないのですが、! を付ける事で、バリデーションエラーを例外エラー(赤い画面のやつ)として拾うようになります。

バリデー失敗しました: 開始日を入力してください となっていますので、データーを入れてみましょう。

irb(main):008> task.start_time = Time.current
=> Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00

値の代入後は、戻り値である現在の日時しか表示されませんので、次で現状確認をしています。

irb(main):009> task
=>
#<Task:0x00000001088cb910
id: nil,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>

日時が入りました。

では、これを save してみます。

現在インスタンスは、変数 task の中に入っていますので、

irb(main):010> task.save!
  TRANSACTION (0.2ms)  begin transaction
  Task Create (2.6ms)  INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [["title", "test"], ["description", "Hello World!"], ["start_time", "2023-11-17 05:53:53.809442"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 06:00:30.446569"], ["updated_at", "2023-11-17 06:00:30.446569"]]
  TRANSACTION (1.6ms)  commit transaction
=> true

saveがうまくいった時の戻り値は true となり、

INSERT INTO "tasks"

のようにSQLがレコードされるログが出てきますので、ここでようやくDBにアクセスしたことになります。

また、 TRANSACTION (1.6ms) commit transaction というログで、DB上でも問題なくレコードが終了したことが分かります。

create

createnewsave を一緒に行います。

irb(main):011> Task.create(title: "test2", description: "荷物を送る", start_time: Time.current)
TRANSACTION (0.1ms)  begin transaction
Task Create (0.5ms)  INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [["title", "test2"], ["description", "荷物を送る"], ["start_time", "2023-11-17 06:51:40.954905"], ["end_time", nil], ["status", 0created_at", "2023-11-17 06:51:40.955624"], ["updated_at", "2023-11-17 06:51:40.955624"]]
TRANSACTION (1.0ms)  commit transaction
=>
#<Task:0x0000000108b26908
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>

save が成功した時の戻り値は true でしたが、 create が成功した時の戻り値はモデルオブジェクトそのものとなります。

first・last

次に登録したレコードを呼び出してみます。

irb(main):012> Task.last
  Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 
#<Task:0x0000000108c45280
 id: 2,
 title: "test2",
 description: "荷物を送る",
 start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
 end_time: nil,
 status: "not_started",
 created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
 updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>

last は直前に登録したレコードを呼び出します。

逆に最初のレコードを呼び出す為には、 first を使います。

irb(main):013> Task.first
  Task Load (0.2ms)  SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 
#<Task:0x00000001088c7810
 id: 1,
 title: "test",
 description: "Hello World!",
 start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
 end_time: nil,
 status: "not_started",
 created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
 updated_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00>

find

find は 登録された id からレコードを呼び出します。

irb(main):014> Task.find(1)
  Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> 
#<Task:0x0000000108c08d58
 id: 1,
 title: "test",
 description: "Hello World!",
 start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
 end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
 status: "not_started",
 created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
 updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>

レコードが存在しない場合は例外エラーとなります。

irb(main):015> task = Task.find(100)
  Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ?  [["id", 100], ["LIMIT", 1]]
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/core.rb:252:in `find': Couldn't find Task with 'id'=100 (ActiveRecord::RecordNotFound)

find_by

find_by は 呼び出すレコードをカラムを指定して行なえます。

irb(main):016> Task.find_by(title: "test")
Task Load (0.2ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."title" = ? LIMIT ?  [["title", "test"], ["LIMIT", 1]]
=>
#<Task:0x0000000108aa0f10
id: 1,
title: "test",
description: "Hello World!",
start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>

レコードが存在しない場合は nil となります。

irb(main):017> Task.find_by(id: 100)
  Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ?  [["id", 100], ["LIMIT", 1]]
=> nil

update

次に登録済みのレコードを編集します。

end_time にも値を入れてみましょう。

今回は、先に変数 taskTask.first を代入します。

irb(main):018> task = Task.first
Task Load (0.2ms)  SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>
#<Task:0x0000000108be5650
...

現在時刻から一日後の時間を入れますので、 Time.current.tomorrow とします。

irb(main):019> task.update(end_time: Time.current.tomorrow)
TRANSACTION (0.1ms)  begin transaction
Task Update (0.6ms)  UPDATE "tasks" SET "end_time" = ?, "updated_at" = ? WHERE "tasks"."id" = ?  [["end_time", "2023-11-18 07:02:38.622984"], ["updated_at", "2023-11-17 07:02:38.625491"], ["id", 1]]
TRANSACTION (0.3ms)  commit transaction
=> true

この時の戻り地は true となります。

all

全てのレコードを取得するには、 all を使います。

irb(main):020> Task.all
  Task Load (0.2ms)  SELECT "tasks".* FROM "tasks" /* loading for pp */ LIMIT ?  [["LIMIT", 11]]
=> 
[#<Task:0x0000000108c4c080
  id: 1,
  title: "test",
  description: "Hello World!",
  start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
  end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
  status: "not_started",
  created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
  updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>,
 #<Task:0x0000000108c4bf40
  id: 2,
  title: "test2",
  description: "荷物を送る",
  start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
  end_time: nil,
  status: "not_started",
  created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
  updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>]

複数のレコードが配列 [] の中に入っています。

where

where は複数のレコードを探す事が出来ます。

初期ステータス not_started のレコードを探すなら

irb(main):021> Task.where(status: "not_started")
  Task Load (0.2ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."status" = ? /* loading for pp */ LIMIT ?  [["status", 0], ["LIMIT", 11]]
=> 
[#<Task:0x0000000108c46f40
  id: 1,
  title: "test",
  description: "Hello World!",
  start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
  end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
  status: "not_started",
  created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
  updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>,
 #<Task:0x0000000108c46e00
  id: 2,
  title: "test2",
  description: "荷物を送る",
  start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
  end_time: nil,
  status: "not_started",
  created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
  updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>]

このようになりますし、 end_time に値が入っているものなら、

irb(main):022> Task.where.not(end_time: nil)
  Task Load (0.2ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."end_time" IS NOT NULL /* loading for pp */ LIMIT ?  [["LIMIT", 11]]
=> 
[#<Task:0x0000000108c4b180
  id: 1,
  title: "test",
  description: "Hello World!",
  start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
  end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
  status: "not_started",
  created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
  updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>]

のように nil でないものと書く事ができます。

destroy

レコードの削除は、 destroy を使います。

変数 task にインスタンスを入れて、 destroy しています。

irb(main):023> task = Task.last
Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ?  [["LIMIT", 1]]
=>
#<Task:0x0000000108bc0dc8
...
irb(main):024> task.destroy
TRANSACTION (0.1ms)  begin transaction
Task Destroy (0.4ms)  DELETE FROM "tasks" WHERE "tasks"."id" = ?  [["id", 2]]
TRANSACTION (0.7ms)  commit transaction
=>
#<Task:0x0000000108bc0dc8
id: 2,
title: "test2",
description: "荷物を送る",
start_time: Fri, 17 Nov 2023 06:51:40.954905000 UTC +00:00,
end_time: nil,
status: "not_started",
created_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00,
updated_at: Fri, 17 Nov 2023 06:51:40.955624000 UTC +00:00>

これは、下記のように直接書いてもOKです。

irb(main):025> Task.last.destroy
  Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" ORDER BY "tasks"."id" DESC LIMIT ?  [["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  Task Destroy (0.5ms)  DELETE FROM "tasks" WHERE "tasks"."id" = ?  [["id", 1]]
  TRANSACTION (0.1ms)  commit transaction
=> 
#<Task:0x0000000108c42300
 id: 1,
 title: "test",
 description: "Hello World!",
 start_time: Fri, 17 Nov 2023 05:53:53.809442000 UTC +00:00,
 end_time: Sat, 18 Nov 2023 07:02:38.622984000 UTC +00:00,
 status: "not_started",
 created_at: Fri, 17 Nov 2023 06:00:30.446569000 UTC +00:00,
 updated_at: Fri, 17 Nov 2023 07:02:38.625491000 UTC +00:00>

ただし、レコードがない場合は

irb(main):026> Task.find(100).destroy
  Task Load (0.1ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ?  [["id", 100], ["LIMIT", 1]]
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/core.rb:252:in `find': Couldn't find Task with 'id'=100 (ActiveRecord::RecordNotFound)

例外エラーとなります。

destroy_all

テーブルのレコード全て削除する時は destroy_all を使います。

先に、レコードを10件作ります。 each を使ってレコードの作成を10回繰り返します。

irb(main):027> (1..10).each{|i| Task.create!(title: "test#{i}", start_time: Time.current) }
  TRANSACTION (0.1ms)  begin transaction
  Task Create (2.0ms)  INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [["title", "test1"], ["description", nil], ["start_time", "2023-11-17 07:40:35.348580"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:40:35.349055"], ["updated_at", "2023-11-17 07:40:35.349055"]]
  TRANSACTION (0.3ms)  commit transaction
  TRANSACTION (0.0ms)  begin transaction
  Task Create (0.2ms)  INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [["title", "test2"], ["description", nil], ["start_time", "2023-11-17 07:40:35.352315"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:40:35.352533"], ["updated_at", "2023-11-17 07:40:35.352533"]]
  TRANSACTION (0.1ms)  commit transaction
  (略)
  , ?, ?) RETURNING "id"  [["title", "test10"], ["description", nil], ["start_time", "2023-11-17 07:40:35.361751"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:40:35.361886"], ["updated_at", "2023-11-17 07:40:35.361886"]]
  TRANSACTION (0.1ms)  commit transaction
=> 1..10

現在10件のレコードがあります。

irb(main):028> Task.count
  Task Count (0.2ms)  SELECT COUNT(*) FROM "tasks"
=> 10

これを削除します。

irb(main):029> Task.destroy_all
  Task Load (0.2ms)  SELECT "tasks".* FROM "tasks"
  TRANSACTION (0.0ms)  begin transaction
  Task Destroy (0.2ms)  DELETE FROM "tasks" WHERE "tasks"."id" = ?  [["id", 3]]
  TRANSACTION (0.1ms)  commit transaction
  TRANSACTION (0.0ms)  begin transaction
  Task Destroy (0.2ms)  DELETE FROM "tasks" WHERE "tasks"."id" = ?  [["id", 4]]
  TRANSACTION (0.1ms)  commit transaction
  TRANSACTION (0.0ms)  begin transaction
 (略)
  #<Task:0x0000000108aaa010
  id: 12,
  title: "test10",
  description: nil,
  start_time: Fri, 17 Nov 2023 07:40:35.361751000 UTC +00:00,
  end_time: nil,
  status: "not_started",
  created_at: Fri, 17 Nov 2023 07:40:35.361886000 UTC +00:00,
  updated_at: Fri, 17 Nov 2023 07:40:35.361886000 UTC +00:00>]

削除できました。数も確認します。

irb(main):030> Task.count
  Task Count (0.2ms)  SELECT COUNT(*) FROM "tasks"
=> 0

ここで、削除したものはテーブルを落とした訳ではないので、

irb(main):031> Task.create(title: "test", start_time: Time.current)
  TRANSACTION (0.1ms)  begin transaction
  Task Create (0.6ms)  INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [["title", "test"], ["description", nil], ["start_time", "2023-11-17 07:49:17.172420"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-17 07:49:17.173004"], ["updated_at", "2023-11-17 07:49:17.173004"]]
  TRANSACTION (0.3ms)  commit transaction
=> 
#<Task:0x0000000108b8d4a0
 id: 13,
 title: "test",
 description: nil,
 start_time: Fri, 17 Nov 2023 07:49:17.172420000 UTC +00:00,
 end_time: nil,
 status: "not_started",
 created_at: Fri, 17 Nov 2023 07:49:17.173004000 UTC +00:00,
 updated_at: Fri, 17 Nov 2023 07:49:17.173004000 UTC +00:00>

id 自体は初期化されず、続きの id (上の例だと 13) が採番されています。

これを初期化するには、一旦

irb(main):032> exit

で、コンソールを脱出したあと、 rails db:migrate:reset をすれば戻ります。

 todo_app % rails db:migrate:reset
Dropped database 'storage/development.sqlite3'
Dropped database 'storage/test.sqlite3'
Created database 'storage/development.sqlite3'
Created database 'storage/test.sqlite3'
== 20231105023538 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0008s
== 20231105023538 CreateTasks: migrated (0.0008s) =============================

== 20231108012222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0007s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0003s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0003s
== 20231108012222 DeviseCreateUsers: migrated (0.0014s) =======================

 todo_app % 

コンソールでの操作はここまでです。

2. バリデーションとは

バリデーションは、モデル側で扱うデーターの制約を掛けていく仕組みで、意図しないデーターの入力を防ぐ為のものです。

すでに、↓こちらで少し記述はしてあるのですが、

ここではもう少し掘り下げて、動きの確認をしてみましょう。

validates

最初に、以前記述したバリデーションの記述を確認します。

app/models/task.rb【確認】

class Task < ApplicationRecord
  enum status: { not_started: 0, in_progress: 1, closed: 2 }

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true
end

現在は、 title description start_time に対して書かれています。

簡単に今の仕様を書き出すと、

カラム名 仕様 使うオプション
title 入力は必須
3文字以上20文字以内
presence
length
description 入力は任意
3文字以上500文字以内
allow_blank
length
start_time 入力は必須 presence

となっています。

この検証を先程のRailsコンソールで行ってみましょう。

3. errorsとは

errors とは、バリデーションが実行されたタイミングでインスタンスに記録される項目です。 valid? メソッド実行時にに記録されます。

valid?

では、コンソールを開きます。

 todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001> 

新しくインスタンスを作ります。

irb(main):001> Task.new
=> 
#<Task:0x00000001085dbef8
 id: nil,
 title: nil,
 description: nil,
 start_time: nil,
 end_time: nil,
 status: "not_started",
 created_at: nil,
 updated_at: nil>

コンソールは で履歴を呼び出せますので、呼び出して書き足す感じで進めるとスムーズです。

呼び出して、 valid? を付け加えます。

irb(main):002> Task.new.valid?
=> false

false が返ってきました。 この場合、バリデーションに不備があるという事です。

では、その不備を確認していきましょう。

errors

errors メソッドを使います。 そのために一旦インスタンスを変数に入れてから valid? errors を実行していきます。

irb(main):003> task = Task.new
=> 
#<Task:0x0000000102e03708
...
irb(main):004> task.valid?
=> false
irb(main):005> task.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=title, type=blank, options={}>, #<ActiveModel::Error attribute=title, type=too_short, options={:count=>3}>, #<ActiveModel::Error attribute=start_time, type=blank, options={}>]>

ActiveModel::Errors というオブジェクトが表示されています。この中に対象のエラーの情報が入ってきます。

内容は

type=too_short, options={:count=>3}>, #<ActiveModel::Error attribute=start_time, type=blank, options={}>

と、なんとなく分からなくもないですが、ちょっと見にくいですね。

これをもう少し見やすくしましょう。

full_messages

エラーを整形して表示させるには、 モデル名.errore.full_messages と書きますので、

irb(main):006> task.errors.full_messages
=> ["タイトルを入力してください", "タイトルは3文字以上で入力してください", "開始日を入力してください"]

と、このように書けば、 I18nも適用された状態でバリデーションメッセージを見る事が出来ます。

errors.add

また、この errors はエラーを追加する事もできます。

irb(main):007> task.errors.add(:title, "ほげほげ")
=> #<ActiveModel::Error attribute=title, type=ほげほげ, options={}>
irb(main):008> task.errors.full_messages
=> ["タイトルを入力してください", "タイトルは3文字以上で入力してください", "開始日を入力してください", "タイトルほげほげ"]

これは、Railsが用意している以外で、カスタムバリデーションの記述などに使えます。(今は使いません)

では、次に指摘された titlestart_time にデーターを入れていきましょう。

すでに変数にインスタンスを代入していますので、

task.title = "test"
task.start_time = Time.current

のように一つずつ追加しても良いですし、複数項目追加するなら assign_attributes を使うとすっきり書けます。

irb(main):008> task.assign_attributes(title: "test", start_time: Time.current)
=> nil

これで、再度 valid? を使うと

irb(main):009> task.valid?
=> true

true が返ってきました。

これで save が可能です。

irb(main):010> task.save
TRANSACTION (0.2ms)  begin transaction
Task Create (2.2ms)  INSERT INTO "tasks" ("title", "description", "start_time", "end_time", "status", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING "id"  [["title", "test"], ["description", nil], ["start_time", "2023-11-18 02:06:16.141775"], ["end_time", nil], ["status", 0], ["created_at", "2023-11-18 02:19:40.641577"], ["updated_at", "2023-11-18 02:19:40.641577"]]
TRANSACTION (0.8ms)  commit transaction
=> true

検証はここまでです。 exit でコンソールを終了しておきましょう。

4. エラーメッセージ

ここまでを踏まえて、フォームの入力に対して、バリデーションメッセージを表示する記述をしていきましょう。

エラーメッセージのテンプレート

エラーメッセージをテンプレート化したいので、_error_messages.html.erb として作っておきましょう。

app/views/layouts/_error_messages.html.erb

<% if model.errors.any? %>
  <ul class="alert alert-warning" style="list-style:none;">
    <% model.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

インスタンスを model という変数で渡してくる前提で、errors でエラー情報に配列の要素が存在すれば、 バリデーションに引っかかっているという事ですので、

errors.any? でバリデーションエラーが存在する時に full_messages を取得し、 each で1つずつ取り出してリスト表示させます。

エラーメッセージの呼び出し側

tasks/_form.html.erb

Taskモデルのフォーム側にこれを呼び出す記述を書いていきましょう。

app/views/tasks/_form.html.erb【確認】

<div class="row">
  <div class="col-md-6">
    <%= form_with(model: task) do |form| %>
      <!-- ここから -->
      <% if task.errors.any? %>
        <div style="color: red">
          <h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>

          <ul>
            <% task.errors.each do |error| %>
              <li><%= error.full_message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>
      <!-- ここまで -->
      <% if params[:action] == "new" || params[:action] == "create" %>
        <div class="mb-3">
          <%= form.label :title, style: "display: block" %>
          <%= form.text_field :title, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :description, style: "display: block" %>
          <%= form.text_area :description, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :start_time, style: "display: block" %>
          <%= form.datetime_field :start_time, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :end_time, style: "display: block" %>
          <%= form.datetime_field :end_time, class: "form-control" %>
        </div>
      <% end %>

      <% if params[:action] == "edit" %>
        <div class="mb-3">
          <%= form.label :status, style: "display: block" %>
          <%= form.select :status, Task.statuses.keys.map {|k| [I18n.t("enums.task.status.#{k}"), k]}, {}, { class: "form-control" } %>
        </div>
      <% end %>

      <div class="mb-3">
        <%= form.submit class: "btn btn-primary" %>
      </div>
    <% end %>
  </div>
</div>

この中でもすでに scaffold が用意している該当コードがあり、

      <% if task.errors.any? %>
        <div style="color: red">
          <h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>

          <ul>
            <% task.errors.each do |error| %>
              <li><%= error.full_message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>

この部分を先程作ったテンプレートの呼び出しに置き換えていきましょう。

上記のコードを、以下のコードに置き換えます。

<%= render "layouts/error_messages", model: form.object %>

引数に model: form.object と書いています。

model: で呼び出すテンプレート側で使いたい変数 model をキーとして渡して、その中身である 該当するインスタンス(@task) をインスタンス変数で渡さずに、
form_with で使うヘルパーのブロック変数 form からモデルのインスタンスを取り出す object で呼び出しています。

一見無駄のような呼び出し方をしていますが、これは、呼び出し元の変数名に依存させなくする事で、テンプレートを汎用的に使うために行うための方法です。

編集後は以下のようになります。

app/views/tasks/_form.html.erb【編集】

<div class="row">
  <div class="col-md-6">
    <%= form_with(model: task) do |form| %> 
      <%= render "layouts/error_messages", model: form.object %><!-- こうする -->
    
      <% if params[:action] == "new" || params[:action] == "create" %>
        <div class="mb-3">
          <%= form.label :title, style: "display: block" %>
          <%= form.text_field :title, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :description, style: "display: block" %>
          <%= form.text_area :description, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :start_time, style: "display: block" %>
          <%= form.datetime_field :start_time, class: "form-control" %>
        </div>

        <div class="mb-3">
          <%= form.label :end_time, style: "display: block" %>
          <%= form.datetime_field :end_time, class: "form-control" %>
        </div>
      <% end %>

      <% if params[:action] == "edit" %>
        <div class="mb-3">
          <%= form.label :status, style: "display: block" %>
          <%= form.select :status, Task.statuses.keys.map {|k| [I18n.t("enums.task.status.#{k}"), k]}, {}, { class: "form-control" } %>
        </div>
      <% end %>

      <div class="mb-3">
        <%= form.submit class: "btn btn-primary" %>
      </div>
    <% end %>
  </div>
</div>

では、他のフォームにも適用しておきましょう。

当然 Devise のテンプレートにも使えますので、

users/registrations/new.html.erb

app/views/users/registrations/new.html.erb【確認】

<h2>Sign up</h2>

<div class="row">
  <div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 col-sm-8 offset-sm-2">
    <div class="card">
      <div class="card-body">
        <%= form_with(model: @user, url: registration_path(resource_name)) do |f| %>
          <%= render "users/shared/error_messages", resource: resource %><!-- ここ -->

          <div class="mb-3">
            <%= f.label :email, class: "form-label" %>
            <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
          </div>

          <div class="mb-3">
            <%= f.label :password, class: "form-label" %>
            <% if @minimum_password_length %>
              <em>(<%= @minimum_password_length %> characters minimum)</em>
            <% end %><br />
            <%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
          </div>

          <div class="mb-3">
            <%= f.label :password_confirmation, class: "form-label" %>
            <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
          </div>

          <div class="mb-3">
            <%= f.submit "Sign up", class: "btn btn-primary w-100" %>
          </div>
        <% end %>
      </div>
    </div>
  </div>
</div>


<%= render "users/shared/links" %>

すでに Devise のエラーメッセージを呼びにいっていますが、これを今作ったものを見るように変更しましょう。

以下のコードに変更します。

<%= render "layouts/error_messages", model: f.object %>

渡す変数のキー model に対する値は、 form ではなく f となっていますね。これは、フォームのテンプレートの問題で scaffoldform としているのに対して、 Devisef としてテンプレートを生成しているためです。

scaffoldを使わず書く場合などは、通常 f としている事が多いように思います。

書き換え後は以下のようになります。

app/views/users/registrations/new.html.erb【編集】

<h2>Sign up</h2>

<div class="row">
  <div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 col-sm-8 offset-sm-2">
    <div class="card">
      <div class="card-body">
        <%= form_with(model: @user, url: registration_path(resource_name)) do |f| %>
          <%= render "layouts/error_messages", model: f.object %><!-- こうする -->

          <div class="mb-3">
            <%= f.label :email, class: "form-label" %>
            <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
          </div>

          <div class="mb-3">
            <%= f.label :password, class: "form-label" %>
            <% if @minimum_password_length %>
              <em>(<%= @minimum_password_length %> characters minimum)</em>
            <% end %><br />
            <%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
          </div>

          <div class="mb-3">
            <%= f.label :password_confirmation, class: "form-label" %>
            <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
          </div>

          <div class="mb-3">
            <%= f.submit "Sign up", class: "btn btn-primary w-100" %>
          </div>
        <% end %>
      </div>
    </div>
  </div>
</div>


<%= render "users/shared/links" %>

編集箇所は以上です。

todo_app % bin/dev

でサーバーを起動して確認すると、以下のようになっているはずです。

Image from Gyazo

Image from Gyazo

これで、バリデーションエラーを1箇所で管理出来るようになりました。

5. git

git

ここまでを保存しておきましょう。

todo_app % git add .
todo_app % git commit -m "バリデーションメッセージのテンプレート化"

GitHub

Githubの設定ができているのであれば、同名のリポジトリを作成し、pushしておきましょう。

おわりに

本チャプターはここまでとなります。

次回は

  • Enumのバリデーション
  • カスタムバリデーション
  • 日時のバリデーション
  • タイムゾーン

を学びます。

ここまでお疲れ様でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0