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

## はじめに

### 前回は

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

https://qiita.com/tomoaki-kimura/items/7417b23f25db370ca619

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

について学びました。

### 今回は

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

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

について学びます。

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

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

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

### rails c

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

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

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

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

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

```zsh
todo_app % rails db:migrate:reset
```

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

```zsh
 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) =======================
```

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

```zsh
todo_app % rails c
```

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

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

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

### new

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

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

```zsh
irb(main):001> Task.new
````

このように書きます。

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

```zsh
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` 以外の入力がありません。

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

#### 引数で渡す

```zsh
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>
```

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

```zsh
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の各テーブルにインスタンスの値をレコードします。

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

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

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

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

その場合、

```zsh
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)
```

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

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

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

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

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


```zsh
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` の中に入っていますので、

```zsh
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

`create` は `new` と `save` を一緒に行います。

```zsh
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

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

```zsh
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` を使います。

```zsh
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` からレコードを呼び出します。

```zsh
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>

```

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

```zsh
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` は 呼び出すレコードをカラムを指定して行なえます。

```zsh
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` となります。

```zsh
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` にも値を入れてみましょう。

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

```zsh
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` とします。

```zsh
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` を使います。

```zsh
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` のレコードを探すなら

```zsh
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` に値が入っているものなら、

```zsh
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` しています。

```zsh
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です。

```zsh
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>
```

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

```zsh
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回繰り返します。

```zsh
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件のレコードがあります。

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

これを削除します。

```zsh
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>]
```

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

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

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

```zsh
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) が採番されています。

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

```zsh
irb(main):032> exit
```

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

```zsh
 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. バリデーションとは

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

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

https://qiita.com/tomoaki-kimura/items/94c167b438e0485e2262

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

### validates

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

app/models/task.rb【確認】
```ruby
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       | 入力は必須<br>３文字以上２０文字以内  | presence<br>length    |
| description | 入力は任意<br>３文字以上５００文字以内 | allow_blank<br>length ||
| start_time  | 入力は必須                 | presence              |

となっています。

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

## 3. errorsとは

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

### valid?

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

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

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

```zsh
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?` を付け加えます。

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

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

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

### errors

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

```zsh
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` と書きますので、

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

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

### errors.add

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

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

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

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

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

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

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

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

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

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

`true` が返ってきました。

これで `save` が可能です。

```zsh
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
```ruby
<% 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` で１つずつ取り出してリスト表示させます。

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

### tasks/_form.html.erb

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

app/views/tasks/_form.html.erb【確認】
```html
<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` が用意している該当コードがあり、

```html
      <% 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【編集】
```html
<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【確認】
```html
<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` のエラーメッセージを呼びにいっていますが、これを今作ったものを見るように変更しましょう。

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

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

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

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

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

app/views/users/registrations/new.html.erb【編集】
```html
<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" %>
```

編集箇所は以上です。

```zsh
todo_app % bin/dev
```

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

[![Image from Gyazo](https://i.gyazo.com/a7f809c20445dc0366925b093b9d4ca8.png)](https://gyazo.com/a7f809c20445dc0366925b093b9d4ca8)

[![Image from Gyazo](https://i.gyazo.com/8a5d1d02d57e2fb03ef23d1d7549a539.png)](https://gyazo.com/8a5d1d02d57e2fb03ef23d1d7549a539)

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

## 5. git

### git

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

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

### GitHub

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

## おわりに

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

### 次回は

https://qiita.com/tomoaki-kimura/items/50877f932b64b365b4cf

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

を学びます。

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