Railsチュートリアルを１周した人向け！招待機能を自作する。

#この記事で実装する機能


- アプリに招待したい人に招待メールを送り、メールの中のURLをクリックした人を新規ユーザーとしてアクティベートする。
- 招待メールの有効期限は24時間
- 招待された人がアカウントをアクティベートした場合、招待したユーザーと招待されたユーザーはお互いをフォローする。


#経緯

Railsチュートリアルを一周した後、二週目はチュートリアルのコードを参考にしながら自作アプリを作るのって、割と効率的な学習法だと思います。
受動的に写経するよりも、主体的にコードにアレンジを加えたほうが理解が深まりますしね。

ただ、この学習法には１つ問題があります。

**アカウント認証のデファクトっぽいgem「Devise」が使えないことです。**

Railsチュートリアルではログイン機能を自作しますが、そのせいでDeviseを導入しようとすると、チュートリアルの中で定義した`current_userメソッド`やら`has_secure_password`で自動生成される`authenticateメソッド`が競合して、色々と問題が起きます。

というか、起きました。

実は、自作アプリで招待機能を実装しようとして、検索上位に出てくるgem devise_invitableを使うためにdeviseを導入しようとしたのですが、
https://qiita.com/himatani/items/907900f1f0d0e4f379b5

結果として、無理でした。
以下のリンクなど色々と試してみたものの、、
https://qiita.com/rrr/items/f4bd8be2772b006987d5
http://ariarichan.hateblo.jp/entry/2016/06/15/235715

結局、「自作のログイン機能を破棄しなければ招待機能を実装できない」という結論に至りました。

「それならいっそ招待機能も自作するわ！！＃＃」ということで車輪の再発明に挑戦してみたところ、チュートリアルの11,12章の知識の組み合わせでなんとかできました。


というわけで、その車輪の実装方法を共有しておこうと思います。



#招待機能の作り方

まずは11章、12章でそうしたように、招待機能のmigrationファイルを作成しましょう。

```ruby:command_line
$rails g migration add_invitation_to_user
```

miragtionファイルの中身はこんな感じ⬇️
invited_byは、招待後に招待したユーザーと招待されたユーザを相互フォローするためのカラムなので、不必要な人はよしなに〜。

```20180910092822_add_invitation_to_users.rb
class AddInvitationToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :invite_digest, :string
    add_column :users, :invited_by, :integer
    add_column :users, :invite_sent_at, :datetime
  end
end
```

マイグレート！

```ruby:command_line
$rails db:migrate
```

からのコントローラ作成。

```ruby:command_line
$rails g controller invitations
```

中身はこんな感じ⬇︎

```invitations_controller.rb
class InvitationsController < ApplicationController
  before_action :get_user,         only: [:edit, :update]
  before_action :valid_user,       only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update]

  def new

  end

  def create

    if params[:invitee][:email].blank?
      flash[:danger] = "メールアドレスを入力してください。"
      render 'new'
    elsif User.find_by(email: params[:invitee][:email])
      flash.now[:danger] = "そのメールアドレスはすでに招待済みです。"
      render 'new'
    else
      @user = User.create(name: "名無しの招待者", email: params[:invitee][:email].downcase, password: "foobar", invited_by: current_user.id)
      @user.create_invite_digest
      @user.send_invite_email
      flash[:info] = "招待メールを送信しました！"
      redirect_to root_url
    end
  end

  def edit
    @user.name = nil if @user
  end

  def update
    if params[:user][:password].empty?
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update_attributes(user_params)
      @user.activate
      log_in @user
      inviter = User.find(@user.invited_by)
      @user.follow(inviter)
      inviter.follow(@user)
      flash[:success] = "ようこそ01Booksへ!"
      redirect_to @user
    else
      render "edit"
    end

  end

  private

  def user_params
    params.require(:user).permit(:name, :password, :password_confirmation)
  end


  #beforeフィルタ

  def get_user
    @user = User.find_by(email: params[:email])
  end

  #正しいユーザーかどうか確認する
  def valid_user
    unless (@user && !@user.activated? &&
        @user.authenticated?(:invite, params[:id])) #params[:id]はメールアドレスに仕込まれたトークン
      flash[:danger] = "無効なリンクです。"
      redirect_to root_url
    end
  end

  #トークンが期限切れかどうか確認する
  def check_expiration
    if @user.invitation_expired?
      flash[:danger] = "招待メールの有効期限が切れています。"
      redirect_to root_url
    end
  end

end

```

複雑に見えるかも知れないけど、このコードはチュートリアルの１２章のpassword_resets_controller.rbとほとんど同じなので理解するのは簡単なはず。
違いは、パスワード再設定の場合は、アクティーベーション済みのユーザーを扱ったけど、招待機能では「未アクティベーション」のユーザーを扱っていることくらいですね。
けど、それもアクティベーションを扱ったチュートリアル１１章を理解していれば、そこまで難しくはないはず。

ビューはこんな感じ⬇︎

```invitations/new.html.erb
<% provide(:title, "友人を招待する") %>


<div class="row box invite">
  <h3 class="center">友人を招待する</h3>
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:invitee, url:invitations_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "招待メールを送信", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

```

```invitations/edit.html.erb
<% provide(:title, "Reset password") %>

<div class="row box">
  <h3>アカウントを設定する</h3>
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: invitation_path(params[:id])) do |f| %>
      <%= render "shared/error_messages", object: f.object %>

      <%= hidden_field_tag :email, @user.email %>

      <%= f.label :name, "名前" %>
      <%= f.text_field :name, class: "form-control" %>

      <%= f.label :password, "パスワード" %>
      <%= f.password_field :password, class: "form-control" %>

      <%= f.label :password_confirmation, "パスワードの確認" %>
      <%= f.password_field :password_confirmation, class: "form-control" %>

      <%= f.submit "登録", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

```


さて、invitations_controllerでは、ユーザーモデルに対するメソッドをふんだんに使ってましたよね。
というわけで、user.rb⬇︎

```invitations_controller.rb
class User < ApplicationRecord

  #:invite_tokenを追加。
  attr_accessor :remember_token, :activation_token, :reset_token, :invite_token
  
  #招待メールを送信する
  def send_invite_email
    UserMailer.invitation(self).deliver_now
  end

  #ユーザー招待の属性（トークンとダイジェストと、招待したユーザーのid）を作成する。
  def create_invite_digest
    self.invite_token = User.new_token
    update_attributes(invite_digest: User.digest(invite_token), invite_sent_at: Time.zone.now)
  end

  #招待の期限が切れている場合はtrueを返す
  def invitation_expired?
    self.invite_sent_at < 24.hours.ago
  end
  
end

```

招待メールを送信するには、メーラーが必要ですよね？
ということで、メーラーはこちら⬇︎


```user_mailer.rb
class UserMailer < ApplicationMailer
 

  def invitation(user)
    @user = user
    @inviter = User.find(@user.invited_by)
    mail to: @user.email, subject: "#{@inviter.name}が01Booksに招待しています。"
  end

end


```

メールの内容も必要ですか？

```invitation.html.erb
<h1>01Books</h1>

<p><%= "#{@inviter.name}さんがあなたを01Booksに招待しています。" %></p>
<p>01Booksは、心理学的に正しい本の学習ツールです。</p>
<p>01Booksに参加するには、下のリンクをクリックしてください。</p>
<p>　</p>
<%= link_to "01Booksのメンバーになる", edit_invitation_url(@user.invite_token,
                                                         email: @user.email) %>
<p>　</p>
<p>このリンクは24時間で有効期限が切れます。</p>
<p>
  意図せずリンクが切れてしまった場合には、友人にもう一度招待メールを送ってもらうか、お手数ですがTwitterで@kawanji01まで直接DMをください。
</p>


```

あとはどこか招待ボタンを設置したいところに、`<%= link_to "友人を招待する", new_invitation_path %>`てな具合に差し込めば、完成。

ちょっと不親切な説明でしたかね。
でも、Googleで`Rails 招待`で検索してもことごとく実装方法が出てこないのだから、きっとみんなdevise_invitableを使っているのでしょう。

車輪の再発明に需要はないんだと思います。いいことです。
けど、ニッチな需要に応えることができるのもWebの長所であると思うので、不親切ながらindexしておきました。


また今回招待機能を再発明する中で実感したのは、なんだかんだチュートリアルを苦しみながらもきちんと学んでおいてよかったということです。

基本さえきちんと理解しておけば、結構応用できちゃうもんなんですね。

苦しめば苦しむほど、学習内容は身につく。
米国の心理学者ロバート・ビョークは、この学習効果を「望ましい困難」と呼んでいます。