Rails フォローリクエスト機能の実装

　スクールのポートフォリオ制作で、中学生とその保護者をターゲットと想定した学習管理アプリの作成を進めています。
　その中で、保護者が生徒をフォローしている場合にテストの点数などの個人情報が見られるようにしたかったため、フォローリクエストが承認された場合のみフォロー状態となるように実装をしました。
　フォロー機能に関してはrailsチュートリアル含めたくさんの参考記事がありましたが、フォローリクエスト機能に関しては参考になる記事が自分では見つけられなかったため、自分なりに実装案を2つ考えました。2つのうち、今回自分が実装した方法を記載してみようと思います。参考になれば幸いです。

###環境
　ruby 2.5.7
　rails 5.2.4
　(slim-railsを使用しています)

### リレーション
　実装する前はparentsテーブルとstudentsテーブルがあるのみ(deviseで作成済み)です。
　実装後、ER図は最終的には下図のようになります。
<img width="510" alt="スクリーンショット 2020-06-08 19.11.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612823/ddd88cff-e35a-f8a1-72ee-ddce9693d0b7.png">
　follow_requestsテーブル(保護者から生徒に送られるフォローリクエストを保存するテーブル)とparent_followsテーブル(生徒がフォローリクエストを承認した場合にフォロー状態を保存するテーブル)がstudentsとparentsの中間テーブルになっています。
また、今回はネームスペースでstudentとparentを分けています。

### 実装案A
　今回実装したのはこのAの方法です。
1. follow_requestsテーブルにstudent_idとparent_idを追加
2. parent_followsテーブルにstudent_idとparent_idを追加
3. 保護者がフォローリクエスト申請ボタンを押したらfollow_requestsにデータ作成
4. 生徒がフォロー許可を押したらparent_followにデータ作成してfollow_requestsのデータは削除

### 実装案B
　一応案Bも記載しておきます。
1. follow_requestテーブルにstudent_id,parent_idを追加
2. studentテーブルにallowカラム(フォローリクエストを許可したかどうか判定するカラム)、データ型booleanで追加
3. リクエスト承認したらstudentsのallowカラムをtrueに変更、follow_requestのデータは論理削除
4. if parent.student.allowで条件つけてコンテンツ表示

　保護者から複数(父親と母親)からフォローされた時に困るような気がしたのと、アプリケーションの作成途中(この時点で8割方できてました)で論理削除のgemを入れるのがなんとなく嫌だったので採用はしませんでした。

### 実装
簡単なところの詳細は省きます。作るテーブル名等は適宜変更して下さい。
##### 1.各テーブル作成
 follow_requestsテーブルとparent_followsテーブルを作成し、それぞれにstudent_id,parent_idカラムをデータ型integerで追加→migrate。
#####2.各モデルにリレーション記載

```ruby:model/parent_follow.rb,model/follow_request.rb
belongs_to :student
belongs_to :parent
```


```ruby:model/parent.rb,model/student.rb
has_many :follow_requests, dependent: :destroy
has_many :parent_follows, dependent: :destroy
```
#####3.コントローラ作成

```terminal:ターミナル
rails g controller student/FollowRequests
rails g controller student/ParentFollows
rails g controller parent/FollowRequests
```

今回はこの3つを作成します。`parent/ParentFollows`は、保護者側がフォロー承認をすることはなかったり、保護者のフォロー一覧を見る必要もなかったりするため作成していません。

#####4.ルーティング
ネームスペースごとに書きます。

```ruby:routes.rb
namespace :parent do
  resources :students, only: [:show] do
    resource :follow_requests, only:[:create, :destroy]
  end
end

namespace :student do
  resources :students, only: [:show, :edit, :update] do
    resources :parent_follows, only:[:destroy, :show, :index]
    post '/follow_requests/:id' => 'follow_requests#allow', as: 'allow'
    resources :follow_requests, only:[:index, :show, :destroy]
  end
end
```
urlが`/parent/students/:student_id/follow_requests`や`/student/students/:student_id/follow_requests/:id`のようになるようにします。
また、`followrequests#allow`のルーティングは`resources:follow_requests`の上に書いてください。よくありますが、下に書いちゃうとエラーになります。

#####5.保護者がフォローリクエストを送れるようにする
こんな感じになります。
![フォローリクエスト1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612823/34f34e13-c837-1ad4-56f0-807affa692a4.gif)


フォローリクエストを送っているかどうかを判定するメソッドをparentモデルに設定します。今回は`already_requested?`としました。

```ruby:model/parent.rb
has_many :follow_requests
  def already_requested?(student)
    self.follow_requests.exists?(student_id: student.id)
  end
```

viewファイルの記述です。先ほどのメソッドを使って、フォローリクエストを送っているかどうかで表示を切り替えます。

```slim:view/parent/student/search.html.slim(フォローリクエストを送るページ)
- if current_parent.already_requested?(student)
   =link_to "フォローリクエスト取消", parent_student_follow_requests_path(student_id:student.id), method: :delete
- else
   =link_to "フォローリクエスト送信", parent_student_follow_requests_path(student_id:student.id), method: :post
```
上記のフォローリクエストを送るページではどの生徒にフォローリクエストを送るのかわかるようにしておいてください。
自分の場合は検索結果画面で、検索した生徒情報をeachで回しています。

コントローラの記述です。follow_requestsテーブルにデータを入れてsaveしたり、そのデータをdestroyしたりします。

```ruby:controller/parent/follow_requests_controller.rb
  def create
    student = Student.find(params[:student_id])
    request = current_parent.follow_requests.new(student_id: student.id, parent_id: current_parent.id)
    request.save
    redirect_back(fallback_location: parent_root_path) #同じページにリダイレクト。これは任意のページで。
  end

  def destroy
    student = Student.find(params[:student_id])
    request = current_parent.follow_requests.find_by(student_id: student.id, parent_id: current_parent.id)
    request.destroy
    redirect_back(fallback_location: parent_root_path) #同じページにリダイレクト。これは任意のページで。
  end
```

以上でフォローリクエストの送信と取消ができるようになりました。


##### 6.生徒側でフォローリクエストを表示する
フォローリクエストが送れたので、生徒側で確認します。

```ruby:controller/student/follow_requests_controller.rb
def index
    @requests = current_student.follow_requests.all 
     #ログイン中の生徒のフォローリクエストを全て取得
end
```

```ruby:view/student/follow_request.html.slim
table.table.table-condensed
  tr
    td
      |名前
    td
    td
  -@requests.each do |request|
    tr
      td
        =request.parent.name
      td
        = link_to "フォロー承認", student_student_allow_path(student_id:current_student.id, id:request.id), method: :post 
           #次の承認で使うコントローラのパスを記載
      td
        = link_to "フォロー拒否", student_student_follow_request_path(student_id:current_student.id, id:request.id), method: :delete
```

こんな感じで表示されます。
<img width="506" alt="スクリーンショット 2020-06-08 21.35.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612823/f41d8d1d-e195-8d27-7256-2ea58841cc65.png">

##### 7.フォローリクエストを承認・拒否する
コントローラで承認(allow)と拒否(destroy)のアクションを記述します。
(書いてて思いましたが、拒否はrefuseですかね。。。resourceに任せる意味合いでもdestroyにしましたが、ちょっと失敗しました。)

```ruby:controller/student/follow_requests_controller.rb
def allow
    request = FollowRequest.find(params[:id])
    parent = Parent.find_by(id:request.parent_id)
    follow = current_student.parent_follows.new(student_id:current_student.id, parent_id: parent.id)
      #follow＿requestsコントローラーですが、parent_followsのnewメソッドです。
    follow.save #parent_followに保存。
    request.destroy # follow_requestは削除
    redirect_back(fallback_location: student_root_path)
  end

def destroy
    request = FollowRequest.find(params[:id])
    request.destroy
    redirect_back(fallback_location: student_root_path)
end
```
これでフォロー承認を押すとフォローリクエスト一覧から消えて、parent_followsテーブルに保存されます。
![フォロー承認.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612823/caed08b1-5a59-7926-f4f6-f9933dc45a7d.gif)

##### 8.保護者フォロー一覧の作成
確認のため、きちんと保護者からのフォロー一覧ページを作成します。
ここでは一応フォローの解除もできるようにします。

```ruby:controller/student/parent_follows_controller.rb
def index
    @followers = current_student.parent_follows.all
end

def destroy
    follow = ParentFollow.find(params[:id])
    follow.destroy
    redirect_back(fallback_location: student_root_path)
end
```

```ruby:view/student/parent_follows/index.html.slim
table.table.table-condensed
  tr
    td
      |名前
    td
      -@followers.each do |follow|
        tr
          td
            =follow.parent.name
          td
            = link_to "フォロー解除", student_student_parent_follow_path(student_id:current_student.id, id:follow.id), method: :delete
```
きちんとフォロー一覧画面に表示されました。
<img width="440" alt="スクリーンショット 2020-06-08 21.56.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612823/9d0ce0c9-b5e7-7fd3-fb10-7515a0123806.png">

##### 8.フォローしている生徒にフォローリクエストが送れないようにする
フォローしていたらフォローリクエストが送れないようにします。
parentモデルにフォロー済みかどうか判定するメソッドを記述します。今回は`already_followed?`としました。

```model/parent.rb
def already_followed?(student)
   self.parent_follows.exists?(student_id: student.id)
end
```

フォローリクエストを送る画面の記述を少し変更します。

```slim:view/parent/student/search.html.slim(フォローリクエストを送るページ)
- if current_parent.already_followed?(student)
   | すでにフォロー済みです
- elsif current_parent.already_requested?(student)
   =link_to "フォローリクエスト取消", parent_student_follow_requests_path(student_id:student.id), method: :delete
- else
   =link_to "フォローリクエスト送信", parent_student_follow_requests_path(student_id:student.id), method: :post
```
このようになります。
<img width="957" alt="スクリーンショット 2020-06-08 22.03.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612823/d2f32eb5-dda8-839e-f5f1-5ade3a7ec980.png">

## まとめ
以上でフォローリクエスト機能を実装することができました。
やり方はいくらでもあると思いますが、学習歴が数ヶ月の自分で思いつく方法はこのぐらいでした。
もしもっと簡単に実装する方法があったり、上記のコードで誤りや不明点がありましたらコメントかTwitterでリプいただけると幸いです。