Rails7 で TODOアプリを作ろう ④ (statusをenumへ)

## はじめに

### 前回は

#### TaskのViewにBootstrap5を適用として

https://qiita.com/tomoaki-kimura/items/76279fa062a9d0bc57a8

- bootstrap5

について学びました。

### 今回は

#### statusをEnumへ として

- バリデーション
- enum

について学びます。

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

## 1. バリデーション

最初に仕込みとなりますが、最低限のバリデーション設定をしておきます。

### バリデーション

バリデーションとはデーターの入力に対して制限を掛ける事で、空入力や多すぎる文字を防いだり、不正な入力を弾く事ができる機能です。

データーの登録前にチェックしようという事ですね。

### SQLのエラー

現在、データーを空投稿しようとすると、

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

このようなエラーが出ます。

これは、マイグレーションファイルを作った時にDBに「必須入力」の制約を入れている為です。

このエラーを防ぐために、モデルにバリデーションを書き入れておきます。

### Model

バリデーションの記述は `Model` に書いていきます。

現状 `app/models/task.rb` は

app/models/task.rb【確認】
```ruby
class Task < ApplicationRecord
end
```

現状 `class` 以外は何も記述がありません。

実は、モデルのCRUD操作やクエリ操作を行う為の小難しい記述は、継承元の `ApplicationRecord` や、さらにその継承元に書かれていますので、モデル単体には該当のテーブルの操作に関するロジックのみを書いていく事ができます。

では、実際に書いてみましょう。

app/models/task.rb【追記】
```ruby
class Task < ApplicationRecord
  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
```

このように `validates` で宣言を行って、各カラムに対して制約を書いていきます。

`presence: true` は必須入力となり、`length` 入力出来る文字数を制限しています。

`allow_blank` は `空入力` の場合、他のバリデーションをスキップするという記述になります。

ここまで書いて、再び空投稿すると、

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

このようにエラーが表示できるようになります。

ただし、フォームも消えてしまいました。

これは `View` 側に表示されるフォームに条件式を入れていた為です。

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

バリデーションエラーが起こった場合、次の `url` にリクエストしていないので、アクションは `create` アクションにとどまっているためです。

ですので、View側の方にも対策をしておきましょう。

app/views/_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.number_field :status, class: "form-control" %>
        </div>
      <% end %>

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

```html
<% if params[:action] == "new" %>
```

この部分を以下のように書き換えます。

```html
<% if params[:action] == "new" || params[:action] == "create" %>
```

app/views/_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>
(以下略) 
```

こうする事によって、フォームが消える現象を防ぐ事ができます。

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

## 2. Enumでステータスを管理する

次にステータスの表示です。

### Enum

`Enum` とは、`列挙型` と言われ、Railsでフラグ管理等に用いる `integer` 型の数値を管理しやすい `Symbol` 型のkeyと `Integer` 型のvalue を持った `Hash` にする事でメンテナンスをしやすくする仕組みです。

まずは、管理しにくい現状を確認してみましょう。

### データーの初期化

新たにデーターが入っていない状態から始めたいので、一旦DBを初期化します。

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

と実行してDBを一旦作り直しましょう。

```zsh
 todo_app % rails db:migrate:reset
Dropped database 'storage/development.sqlite3'
Database 'storage/test.sqlite3' does not exist
Created database 'storage/development.sqlite3'
Created database 'storage/test.sqlite3'
== 20231027073408 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0012s
== 20231027073408 CreateTasks: migrated (0.0013s) =============================
```

リセットできたようです。続いて、

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

でサーバーを起動し、 `localhost:3000` にアクセスします。

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

`New task` からデーターを入力します。

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

### 表示の不便さ

登録出来たら、 `show` アクションを読み込みますので、

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

このような画面となります。 `url`も `localhost:3000/tasks/1` となっています。

この `Status` が `0` とされている所もDBの制約で、「デフォルト値を０」とするようになっている為です。

本当は、 `0` を `未着手`　としたい目論見があるのですが、現状では `0` って何？という感じになっています。全く意味がわかりませんね。

明らかに変換が必要なので、その手法として今回は `Enum` を使っていきます。

### フォームの不便さ

次に、このTaskの進捗を変更する場合、

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

`Edit this task` を押下して、編集画面に遷移します。

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

こちらのフォームも実際の入力に耐えないものとなっていますね。

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

マイナスの値も自由に入れられていますので、これは改善の必要があります。

希望としては、セレクトボックス等で、 `未着手` `着手中` `完了` などのいくつかのステータスを `選択`　出来るようになれば良いですね。

実装の流れは、下記表のように変換されますが、

| DB上のフラグの数値 | 数字を置き換える為のキー | キーを日本語化したもの |
|------------|--------------|-------------|
| 0          | :not_started | 未着手         |
| 1          | :in_progress | 進行中         |
| 2          | :closed      | 完了          |

段階を追って変換していこうと思います。

### Model

では、早速置き換えるために Enumの記述を `Task` モデルに実装していきます。

Railsでは基本的に、こういったロジック関連は `Model` に書きます。

現状 `app/models/task.rb` は

app/models/task.rb【確認】
```ruby
class Task < ApplicationRecord
  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
```

このようになっています。

では、書いてみましょう。

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

まず、 `enum` の宣言をし、 引数のキーにカラム名である `:status` そしてその値に `{ not_started: 0, in_progress: 1, closed: 2 }` というHashが与えられています。

```ruby
{ not_started: 0, in_progress: 1, closed: 2 }
```

このHashが置き換える数値と値の関係を定義しています。

これで第一段階が完了です。

### View

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

でサーバーを起動しましょう。

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

先程 `0` だった `status` のフラグが、 `not_started` に変わっていますね。

無事、対応するキーが割り当てられた形となります。

### FormHelper

では、次に編集画面も見ていきましょう。

app/views/_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" %>
        <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.number_field :status, class: "form-control" %>
        </div>
      <% end %>

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

ここで少し独特な記法について見ていきます。

フォームを担っている部分は、それぞれ `scaffold` によって自動生成されており、

```html
<%= form_with(model: task) do |form| %>
<% end %>
```

この `form_with` のブロック内の `引数` `form` をレシーバーとして使える `formヘルパー` の記法である

```html
<%= form.text_field :title, class: "form-control" %>

<%= form.text_area :description, class: "form-control" %>

<%= form.datetime_field :start_time, class: "form-control" %>

<%= form.number_field :status, class: "form-control" %>
```

これらの部分によって書き込みが出来るようになっています。ざっくり

| ヘルパーメソッド       | 用途            |
|----------------|---------------|
| text_field     | 文字列の入力        |
| text_area      | 改行を許容する文字列の入力 |
| datetime_field | 日付と時間の入力      |
| number_field   | 数値の入力 |

このような目的のフォームとして指定されています。

ただし、今回使いたいフラグの選択肢に `number_field` はふさわしくありません。

セレクトボックスを生成する `select` を使ってみたいと思います。

書き換える部分は、

app/views/tasks/_form.html.erb【確認】（*コードが長いため一部のみを取り出しています。）

```html
（略）
      <% if params[:action] == "edit" %>
        <div class="mb-3">
          <%= form.label :status, style: "display: block" %>
          <%= form.number_field :status, class: "form-control" %>
        </div>
      <% end %>
（略）
```

この `edit` 時に表示させる部分の

```html
<%= form.number_field :status, class: "form-control" %>
```

この行です。

これを `select` を使って書き換えます。

記法としては、

```html
<%= form.select :カラム名, データーとして入力される要素 %>
```

```html
<%= form.select :status, [0, 1, 2] %>
```

このように書くと数値を直接入力できます。

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

ただし、このまま更新しようとすると、

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

`status` の値がエラーとなってしまいます。

`データーとして入力される要素` には `enum` の記法に従う必要があります。

```html
<%= form.select :status, [:not_started, :in_progress, :closed] %>
```

このように `Enum` のキーだけを渡すと、

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

表示上都合よく整えつつ、データーを送る事ができます。

また、この記述は動的に書くべきですので、

```html
<%= form.select :status, Task.statuses.keys %>
```

となります。

さらに、Bootstrapを適用するには少し癖があって、

```html
<%= form.select :status, Task.statuses.keys, {}, { class: "form-control" } %>
```

と書く必要があります。

`select` のオプションは、書く場所と数を指定されているので、機能別に`{}` で括ってあげないと認識できず、

```html
<%= form.select :カラム名, Enumに設定したキーの配列, { フォームの機能オプション }, { HTMLのオプション } %>
```

このような記法となります。

最終的に、

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

```html
（略）
      <% if params[:action] == "edit" %>
        <div class="mb-3">
          <%= form.label :status, style: "display: block" %>
          <%= form.select :status, Task.statuses.keys, {}, { class: "form-control" } %>
        </div>
      <% end %>
（略）
```

このように書き換えが出来ていると良いでしょう。

すると、

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

Bootstrapのフォームが適用できました。

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

データーの編集も出来たようです。

## 3. git

### git

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

```zsh
todo_app % git add .
todo_app % git commit -m "statusをenumへ"
```

### GitHub

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

## おわりに

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

### 次回は

https://qiita.com/tomoaki-kimura/items/cb66bf13549a105a0c71

- i18n
- enum_help

を学びます。

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