【Ruby on Rails】devise_invitableを使った既存ユーザーへの招待機能の実装

# はじめに
この記事は、初学者がgem「devise_invitable」のカスタマイズに挑戦してみた記事です。チーム(グループ)があるアプリで、新規ユーザー/既存ユーザーのどちらにも招待メールを送り、招待されたユーザーがパスワードを登録した時にチームに所属できるようにしています。
**＊この記事ではdevise_invitableの基本的な実装方法は説明していません。実装済みを前提としています。**


# 経緯（読み飛ばしても🙆‍♀️）
Dive Into Codeというプログラミングスクールの卒業課題で「複数人でタスク管理を行うアプリ」を作成しました。簡単なアプリ紹介としては、複数人が所属するチームを作成し、そこでタスクを管理を行うというものです。
招待したい人に「このチームに入ってよ！」と招待メールを送ることができたらいいなと思い、gem「devise_invitable」を使用することにしました。
##### しかし、問題が。。🫠
アプリのイメージとしては1人のユーザーがたくさんのチームに所属できるというものだったのですが、デフォルトのdevise_invitableの機能では、新規のユーザー（まだアカウントを作っていない人）にしかメールを送ることができず、既存ユーザー（既にアカウントをもっている人）には送れません。（＝1人1つのチームにしか所属できない）
##### ただし、絶妙になんとかなりそう感もある。。🤔
devise_invitableのREADMEの[Sending an invitation after user creation](https://github.com/scambra/devise_invitable#sending-an-invitation-after-user-creation-)を見ると、下記のように書かれていて、なんとかなりそうな気配もありました。（DeepLでの翻訳を追加しています）
> Sending an invitation after user creation(ユーザー作成後の招待状送信)
>You can send an invitation to an existing user if your workflow creates them separately:
(ワークフローで既存のユーザーを個別に作成した場合、そのユーザーに招待状を送信することができます。)
> ````
> user = User.find(42)
> user.invite!(current_user)  # current user is optional to set the invited_by attribute
> ````
..というわけでこのカスタマイズに奮闘した次第です！

# 制作したアプリ
【アプリ名】　**TOPPA!**
【アプリURL】http://43.206.187.158/
【GitHub】https://github.com/Asumi8/TOPPA.git

# 開発環境
MacBook Air (M1, 2020)
Rails 6.1.7
ruby 3.0.1
yarn 1.22.19

# 説明する項目
下記の順で説明します。
- 実装の前提
- 基本的なdevise_invitableの実装について
- invitataions/new.html.erbの変更
- invitations_controller.rbをカスタマイズ
- createアクションのの変更
- createアクションの解説
- updateアクションの変更
- updateアクションの解説


# 実装の前提
### 私が実現したい、招待⇨チームに所属の流れ
招待する側、招待される側の動きとしては、下記のような流れを考えました。
招待された側が招待メールにあるアプリへのリンクをクリック、アプリでパスワードの設定をしたタイミングでチームに所属しているという状態にしたいと思います。

![about.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871208/062110f7-71b5-b776-3344-577a80dd99cd.png)

new, create, edit, updateがそれぞれ呼び出されるタイミングは下記の通りです。
![action.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871208/81efe35e-e719-8717-ade4-81a3b37c9802.png)

### 各テーブルの状況
ユーザーはチームに複数所属でき、チームにも複数のユーザーが所属できる仕様を考えているので、中間テーブルとしてassignsテーブルを用意し、user_idとteam_idを管理するようにしています。

usersテーブルのグレー背景部分はdeviseもしくはdevise_invitableによって追加したカラムです。黄色の背景の`invited_by_team_id`については独自に追加したカラムです。後ほど説明します。
![qiita.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2871208/1a051f36-c162-1fa1-823f-2fb6d9754fc7.png)


# 基本的なdevise_invitableの実装について
基本的な実装方法については他に良い記事が多数ありますので、この記事では紹介しません🙇‍♀️
devise, devise_invitable共に実装済み状態になってから次に進んでください。 
(最後に参考にさせていただいた記事を掲載しております)

# invitataions/new.html.erbの変更
招待メールの送信時に、team_idも送信できるようにinvitataions/new.html.erbファイルに
` <%= f.hidden_field :team_id, :value => (params[:team] || team) %>`を追記。
`params[:team]`もしくは変数`team`が：team_idに入るように記述しています。
変数`team`については「render部分について」で説明しています。
````invitations/new.html.erb
<% resource.class.invite_key_fields.each do |field| -%>
    <div class="field">
        <%= f.label field %>
        <%= f.text_field field, :placeholder => "例：xxx＠xx.com" %>

        <%= f.hidden_field :team_id, :value => (params[:team] || team) %>

    </div>
<% end %>
````

# invitations_controller.rbをカスタマイズ
デフォルトの処理の際は、invitations_controller.rbはsuperで継承するだけですが、今回はここの処理を変更したいので`create`アクション、`update`アクションを変更していきます。

## createアクションのの変更
createは招待したい相手のメールアドレスを入力し、招待メールを送る時に呼び出されるアクションです。
全体としては、下記のように変更しました。
````invitations_controller.rb
class Users::InvitationsController < Devise::InvitationsController

  def create
    @user = User.new
    user_email = params[:user][:email]
    team_id = params[:user][:team_id]
    if User.find_by(email: user_email.downcase).present? #　既存ユーザーの処理
      user_id = User.where(email: user_email).pluck(:id)
      user = User.find(user_id[0])
      user.invite!(current_user)
      user.invited_by_team_id = team_id
      user.save
      redirect_to teams_path(current_user), notice: "招待メールが#{user_email}に送信されました。"
    elsif User.invite!(email: user_email, invited_by_team_id: team_id).valid?　 # 新規ユーザーの処理
      redirect_to teams_path(current_user), notice: "招待メールが#{user_email}に送信されました。"
    else
      flash[:notice] = 'メールアドレスを正しく入力してください。'
      render 'new', locals: { team: team_id }
    end
  end
````
## createアクションの解説
#### `@user`について
`@user`の部分に関しては、newのビューのdevise特有の「resource」を書き換えるために記述しています。
````invitations_controller.rb
  @user = User.new
````
createで定義したのち、ビューの[resource部分](https://github.com/scambra/devise_invitable/blob/master/app/views/devise/invitations/new.html.erb#L3)を`@user`に書き換えます。
これによりredirect_toでエラーがでなくなりました。

````invitations/new.html.erb
<%= form_for(@user, as: resource_name, url: invitation_path(resource_name), html: { method: :post }) do |f| %>
````
#### 既存ユーザーへの招待メール
入力された既存ユーザーの情報を変数`user`に代入し、
`user.invite!(current_user)`のinvite!メソッドで招待メールを送ります。
（READMEの[Sending an invitation after user creation](https://github.com/scambra/devise_invitable#sending-an-invitation-after-user-creation-)で紹介されていたものです）

`user.invited_by_team_id = team_id`は、
updateの時に招待されたチームのidを呼び出してチームに所属させるために、
usersテーブルに「invited_by_team_id」というカラムを新たに作成し、その中にteam_idを入れています。
updateアクションの際に説明しますが、招待された側がパスワード等を登録しupdateアクションが呼び出された時に、team_idを取り出してチームに所属させるためです。

````invitations_controller.rb
if User.find_by(email: user_email.downcase).present?
      user_id = User.where(email: user_email).pluck(:id)
      user = User.find(user_id[0])
      user.invite!(current_user)
      user.invited_by_team_id = team_id
      user.save
      redirect_to teams_path(current_user), notice: "招待メールが#{user_email}に送信されました。"
````

#### 新規ユーザーへの招待メール
`User.invite!(email: user_email, invited_by_team_id: team_id)`でinvite!メソッドを使い新規ユーザーに招待メールを送ります。
invite!（）の記載は、以下のgemのissueを参考にしました。
[::invite! modifies existing users #747](https://github.com/scambra/devise_invitable/issues/747)

````invitations_controller.rb
elsif User.invite!(email: user_email, invited_by_team_id: team_id).valid?
    redirect_to teams_path(current_user), notice: "招待メールが#{user_email}に送信されました。"
````

#### render部分について
`render 'new', locals: {　team: team_id　}`は、
バリデーションにひっかかり、newのビューを再表示する時にteam_idを保持できるように、
変数`team`の中にteam_idをいれています。
````invitations_controller.rb
else
     flash[:notice] = 'メールアドレスを正しく入力してください。'
     render 'new', locals: { team: team_id }
end
````


## updateアクションの変更
updateは、招待されたユーザーが招待メールのリンクをクリックし、パスワード登録をした際に呼び出されるアクションです。
全体としては、下記のとおりに変更しました。
teamsテーブルとusersテーブルは多対多の関係で、中間テーブルのassignsテーブルでチームの所属を管理しています。
````invitations_controller.rb
    user_id = resource.id
    user = User.find(user_id)
    user_assigned_teams = Assign.where(user_id: user_id).pluck(:team_id)
    new_team_assign = []
    new_team_assign << user_assigned_teams
    new_team_assign << user.invited_by_team_id
    new_team_assign = new_team_assign.flatten
    user.update(team_member_ids: new_team_assign)
````

## updateアクションの解説
`user_assigned_teams = Assign.where(user_id: user_id).pluck(:team_id)`で
assginsテーブルから現在所属しているチームの情報を取得し、変数`user_assinged_teams`に代入。
変数`new_team_assign`に、`user_assinged_teams`の値と、
createの際にusersテーブルのinvited_by_team_idカラムに入れていたteam_idを入れ、
所属しているチームのidが全て入った状態になります。
````invitations_controller.rb
 user_assigned_teams = Assign.where(user_id: user_id).pluck(:team_id)
    new_team_assign = []
    new_team_assign << user_assigned_teams
    new_team_assign << user.invited_by_team_id
    new_team_assign = new_team_assign.flatten
````
その`new_team_assign`を使用し、assignsテーブルにあるuserの所属しているチームの情報を更新しています。
````invitations_controller.rb
 user.update(team_member_ids: new_team_assign)
````
以上となります！！
このような手順で考えていたような、新規ユーザー/既存ユーザーに関わらず招待メールを送ることができ、チームに所属させるタイミングをupdateで行うことができました。

# 最後に
今回の記事部分の実装を通して、gemのGitHubに書かれている内容やREADMEをじっくり読むことの大切さを学びました。
また、卒業課題の作成にあたりたくさんのQiita記事に助けられましたが、今回初めて自身が記事を書いてみて、記事を書くということの大変さと学びを共有くださる方々への感謝を改めて感じました。
もし抜けている点や至らぬ点ありましたらご指摘いただけますと幸いです。

# 参考にさせていただいた記事

- invite!メソッド
 [devise_invitableのインスタンスメソッド版invite!は、原則として保存済みのUserに対して使う](https://qiita.com/jnchito/items/689a7069f02ce14d1a3f)

- divise_invitableの基本的な実装
[Railsで、deviseとdevise_invitableをつかって招待機能を実装する](https://qiita.com/himatani/items/907900f1f0d0e4f379b5)
[【Ruby on Rails】devise_invitableを使った招待機能実装編 【初心者向け】](https://qiita.com/tak8_al/items/3b8a0bf506ce3b03576c)
[【Ruby on Rails】 愛犬管理アプリを作ってみたので作り方を解説してみる【初心者向け】](https://qiita.com/tak8_al/items/08274dd9228363642555)