【Rails】【通報機能】ユーザー通報機能を作ってみました

ポートフォリオを作っていた際にwebアプリケーションでよく見かけるユーザー通報機能を実装したいと思い、実装の仕組みを検索してみたのですがほとんどヒントを見つけられませんでした。
なので、自己流でユーザー通報機能を実装してみました。まだまだ初心者のため、諸々間違いがあるかもしれませんが、この記事が少しでも皆さんの助けになると幸いです。（何かお気づきのことがあればコメントお願いいたします> <）

### 【環境】
- Rails 6.1.5.1
- ruby 2.6.3
### 【前提】
- 管理者機能(Admin)を実装しています！
- deviseを使っています！
### 【実現したいこと】
- 自分以外のユーザーを通報できる機能。
- 通報されたユーザーを管理者が確認できる。
- その通報のステータス（対応済み等）を更新できる。
- 管理者の判断でユーザーを退会扱いとする事ができる。そのユーザーはログインできなくなる。
### 【テーブル設計】
|  User  |  Report  |
| ---- | ---- |
|  name  |  reporter_id  |
| email | reported_id |
| user_status |  reason  |
|    |  url  |
|    |  status  |
##### Userテーブル
- Userテーブルは適宜必要なカラムを用意してください。
- user_status →ユーザーのアカウントを管理者が「有効・無効」に切り替えられるようにする
##### Reportテーブル
- reporter_id →通報したユーザー
- reported_id →通報されたユーザー
- reason →通報理由
- url →悪質な投稿等のURLがあれば貼ってもらう為
- status →管理者が通報に対して「対応済み」等、ステータスを変えられるようにする。

★ReportテーブルはUserテーブルの中間テーブルになります。（フォロー・フォロワー機能の時のようなイメージ）
通報する人、通報される人もUserになるので、多対多の状態になります。

カラム型、制約等は下記のようにしました！
```db/migrate/○○_add_columns_to_users.rb 
def change
  #deviseで作ったテーブルにカラムを追加したので、add_columnになってます
  add_column :users, :name, :string
  add_column :users, :user_status, :boolean, default: false
end
```
```db/migrate/○○_create_reports.rb
def change
    create_table :reports do |t|
      t.integer :reporter_id, null: false
      t.integer :reported_id, null: false
      t.text :reason, null: false
      t.text :url
      t.integer :status, default: 0, null: false

      t.timestamps
    end
  end
```

### 【モデル】
##### Userモデル
　`reports`と`reverse_of_reports`は分かりやすいように名前を付けているだけなので、自由に変えて頂いて大丈夫です！
　`class_name: "Report"`は↑で好きな名前をつけたので、ちゃんとReportテーブルを参照してもらうための記述です。
　`foreign_key: ○○`は、　どのカラムを参照するかを指定しています。
```app/models/user.rb
has_many :reports, class_name: "Report", foreign_key: "reporter_id", dependent: :destroy
has_many :reverse_of_reports, class_name: "Report", foreign_key: "reported_id", dependent: :destroy
```
##### Reportモデル
こちらも同様に、分かりやすい名前を`belongs_to :○○`のところで付けて、`class_name: "User"`でUserテーブルを参照するよう指定します。
```app/models/report.rb
belongs_to :reporter, class_name: "User"
belongs_to :reported, class_name: "User"
```

### 【enum】
enumとは、数値に意味を持たせることが出来るものです。
今回は、管理者が通報に対して対応ステータス(未対応・対応済み等）を変更できるようにしたいので、enumを使いたいと思います！ちなみに、この記事では「enum_help」というgemを導入していきますが、gemを使わなくてもenumは使えます。

##### enum_helpを導入する
Gemfileの一番下に`gem "enum_help"`を記述し、`bundle install`をします。
```
#Gemfile

gem "enum_help"
```
##### Reportモデルにenumを定義
Reportテーブルを作成した際に、statusカラムをinteger型で作りました。追加した記述は、`status`がカラム名、`waiting`・`keep`・`finish`は好きに命名したものです。0,1,2はstatusカラムに保存される数字です。
私の場合、
waiting →対応待ち
keep →保留
finish →対応済み
にしました。皆さんのお好みで名前を変えたり数を増やしたりしてくださいね！
```app/models/report.rb
belongs_to :reporter, class_name: "User"
belongs_to :reported, class_name: "User"

enum status: { waiting: 0, keep: 1, finish: 2 } #ここを追加
```
##### アプリケーションを日本語化
このままだと、数字に対応する文字列が英語になってしまうので、日本語化していきたいと思います。
下記ファイルにコードを２つ追加します。これで、アプリケーションの言語が日本語になりました！
※これに伴いdeviseを入れられている方は、deviseのエラーメッセージが「日本語対応していません」のような内容になると思うので、以下の記事を参考にされるとエラーメッセージも日本語化できるかと思います！
https://qiita.com/you8/items/921e0dd1210eb0d158df
```config/application.rb 
  class Application < Rails::Application
    ーー省略ーー
    config.i18n.default_locale = :ja　  #追加
    config.i18n.load_path += Dir[Rails.root.join('config/locales/*.yml').to_s]  #追加 
  end
```
##### ymlファイルを作成、記述
`ja.yml`ファイルを作成し、先程定義した英語に対応する日本語を記述しましょう！
これで、enumはひとまず完了です。
```config/locales/ja.yml
ja:
  enums:
    report:
      status:
        waiting: "対応待ち"
        keep: "保留"
        finish: "対応済み"
```
### 【ルーティング】
私のアプリケーションでは、ユーザー側がpublic、管理者側がadmin、としてコントローラやviewを分けています。
管理者(admin)の方は、一覧(index)、詳細(show)ページを作り、詳細ページからステータスを更新(update)できるようにしたいと思います。
ユーザー(public)の方は、通報したいユーザーの詳細ページから通報作成ページ(new)へのリンクを作り、通報作成(create)できるようにしたいと思います。
```config/routes.rb
Rails.application.routes.draw do
　　#省略
 namespace :admin do
   #省略
   resources :reports, only: [:index, :show, :update]
   resources :users, only: [:index, :show, :update, :edit]
   #この記事ではusersはeditとupdateだけ使います！
 end
　　scope module: :public do
   #省略
   resources :users, only: [:show, :edit, :update, :index] do
     resources :reports, only: [:new, :create]
   end
 end
end
```
### 【コントローラ】
まず、通報を作成するユーザー(public)のコントローラーをみてみましょう。
```app/controllers/public/reports_controller.rb
class Public::ReportsController < ApplicationController
  def new
    @report = Report.new　　#新しい通報の箱を用意
    @user = User.find(params[:user_id])　　#どのユーザーに対する通報なのかparamsで取得する
  end

  def create
    @user = User.find(params[:user_id])　　#どのユーザーに対する通報なのかparamsで取得する
    @report = Report.new(report_params)　　#ストロングパラメータを通す
    @report.reporter_id = current_user.id  #通報者(reporter_id)にcurrent_user.idを代入
    @report.reported_id = @user.id　　#通報される人(reported_id)に上で取得した@user.idを代入
    if @report.save #保存する
      redirect_to user_path(@user), notice: "ご報告ありがとうございます。"
    else
      render "new"
    end
  end

  private

  def report_params
    params.require(:report).permit(:reason, :url)
  end
end
```
続いて管理者側です。通報のコントローラです。
```app/controllers/admin/reports_controller.rb 
class Admin::ReportsController < ApplicationController
  
  def index
    @reports = Report.all  #全ての通報を取得
  end

  def show
    @report = Report.find(params[:id])　　#特定の通報IDを取得
  end

  def update
    @report = Report.find(params[:id])
    if @report.update(report_params) #ストロングパラメータを通して更新
      flash[:notice] = "対応ステータスを更新しました。"
　　　　　　　　　　　　redirect_to request.referer
    end
  end

  private

  def report_params
    params.require(:report).permit(:status)
  end
end
```
管理者用のユーザーコントローラです。ユーザーの有効ステータスを更新できるようにします。
```app/controllers/admin/users_controller.rb
class Admin::UsersController < ApplicationController
  def edit
    @user = User.find(params[:id]) #編集したいuserのIDを取得
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params) #ストロングパラメータを通して更新
      redirect_to admin_user_path(@user), notice: "#{@user.name}さんの会員ステータスを更新しました"
    else
      render "edit"
    end
  end

  private

  def user_params
    params.require(:user).permit(:user_status)
  end
end
```

### 【ビュー】
※bootstrap使ってます！！色々省略したのでこのままだとレイアウトめちゃくちゃです...
まずは、ユーザー詳細ページです。通報作成画面へのリンクを追加します。
```app/views/public/users/show.html.erb
#前後は省略
<% if @user != current_user %> #自分の詳細ページでなければ、通報ボタンを表示させる
  <%= link_to "ユーザーを通報", new_user_report_path(@user), class: "btn btn-danger" %>
<% end %>
```
先程のリンクを押した後の、通報作成画面です。ここで作成したものが管理者側のviewで見れるようになります。
```app/views/public/reports/new.html.erb
<h3>通報</h3>

<%= form_with model:[@user, @report] do |f| %>
  <div class="form-group">
    <%= f.text_area :reason, rows:'3', class:"form-control" %>
    <%= f.text_field :url, class:"form-control" %>
    <%= f.submit "通報する", class: "btn btn-danger" %>
  </div>
<% end %>
```
続いて、管理者の通報一覧ページです。
```app/views/admin/reports/index.html.erb
<h3>通報一覧</h3>

<table class="table">
  <thead>
    <tr>
      <th>通報ID</th>
      <th>会員名</th>
      <th>通報者</th>
      <th>ステータス</th>
    </tr>
  </thead>
  <tbody>
    <% @reports.each do |report| %>
      <tr>
        <td>
          <%= link_to report.id, admin_report_path(report) %>
        </td>
        <td>
          <%= report.reported.name %>
        </td>
        <td>
          <%= report.reporter.name %>
        </td>
        <td>
          <% report.status_i18n %>　　#enumで定義した日本語を呼び出す
        </td>
      </tr>
    <% end %>
  </tbody>
</table>
```
通報詳細ページです。
最後の方にある、`<%= f.select :status, Report.statuses.keys.map { |k| [t("enums.report.status.#{k}"), k] } %>`この記述をすると、Reportテーブルのstatusカラムにenumで定義した日本語バージョンのセレクトボックスを作ることができます。
```app/views/admin/reports/show.html.erb
<h3 class="page-title">通報詳細</h3>

<table class="table">
    <tbody>
      <tr>
        <td>通報ID</td>
        <td><%= link_to @report.id %></td>
      </tr>
      <tr>
        <td>会員名</td>
        <td><%= @report.reported.name %></td>
      </tr>
      <tr>
        <td>理由</td>
        <td><%= @report.reason %></td>
      </tr>
      <tr>
        <td>URL等</td>
        <td><%= @report.url %></td>
      </tr>
      <tr>
        <td>通報者</td>
        <td><%= @report.reporter.name %></td>
      </tr>
      <tr>
        <td>対応ステータス</td>
        <td>
          <%= @report.status_i18n %>
          <%= form_with model:@report, url:admin_report_path(@report), method: :patch, local: true do |f| %>
            <%= f.select :status, Report.statuses.keys.map { |k| [t("enums.report.status.#{k}"), k] } %>
            <%= f.submit '更新' %>
          <% end %>
        </td>
      </tr>
    </tbody>
  </table>
</div>
```
ユーザー情報の編集ページです。user_statusはboolean型で作成したので、trueかfalseを選べるようにします。(デフォルトをfalseにしているので、falseが有効です！)
```app/views/admin/users/edit.html.erb
<h3>会員情報編集</h3>

<%= form_with model:@user, url: admin_user_path(@user), method: :patch do |f| %>
 <div class="custom-control custom-radio">
   <%= f.label :user_status, '会員ステータス' %>
   <%= f.radio_button :user_status, false %> <%= f.label :有効 %>
   <%= f.radio_button :user_status, true %> <%= f.label :退会 %>
 </div>
 <%= f.submit "ステータスを更新" %>
<% end %>
```

### 【ユーザーログイン制限】
最後に、管理者によってステータスを退会に変更されたユーザーがログインできないようにしたいと思います。
セッションのコントローラにログインできないメソッドを記述します。
```/app/controllers/public/sessions_controller.rb
 # 退会済なら新規登録画面へ遷移させる
  def user_state
    @user = User.find_by(email: params[:user][:email])　　#emailからuserを探す
    if @user　　　#もしアカウントが見つかれば
      if @user.valid_password?(params[:user][:password]) && @user.user_status
      #@userのパスワードが有効かつ退会済み(user_status == true)ならば
        flash[:notice] = 'お客様のアカウントは現在ご使用できません。'
        redirect_to new_user_registration_path #新規登録画面へ遷移させる
      end
    end
  end
```

### 【まとめ】
ここまでで、ユーザー通報機能は完成です！
長々と書いてしまいましたが、最後までご覧いただきありがとうございます！！個人的にはもっといい実装方法がある気がするので、もっと勉強していきたいと思います。