LoginSignup
15
6

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-05

はじめに

この記事は、初学者が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を見ると、下記のように書かれていて、なんとかなりそうな気配もありました。(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

new, create, edit, updateがそれぞれ呼び出されるタイミングは下記の通りです。
action.png

各テーブルの状況

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

usersテーブルのグレー背景部分はdeviseもしくはdevise_invitableによって追加したカラムです。黄色の背景のinvited_by_team_idについては独自に追加したカラムです。後ほど説明します。
qiita.drawio.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部分@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で紹介されていたものです)

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

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記事に助けられましたが、今回初めて自身が記事を書いてみて、記事を書くということの大変さと学びを共有くださる方々への感謝を改めて感じました。
もし抜けている点や至らぬ点ありましたらご指摘いただけますと幸いです。

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

15
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
6