Railsで申し込み機能を実装する

Railsを用いた投稿の申し込み機能とキャンセル機能の実装方法について解説します。
※ユーザー機能と投稿機能は実装済みであることを前提とします。

## 実装の流れ
大まかな流れとして、以下の手順で実装していきます。
1. 投稿と申込者の中間テーブルを作成する
2. モデル同士の関連付け（アソシエーション）を行う
3. ルーティングを定義する
4. コントローラーに申し込み機能とキャンセル機能を定義する
5. 申込ボタンのビューを作成する


## 投稿と申込者の中間テーブルを作成する

投稿は複数人の申込者を持つことができ、ユーザーは複数の投稿に申し込むことができます。
そのため、投稿とユーザーは多対多の関係となり、中間テーブルが必要となります。

また、ここでは同じユーザーの中でも投稿者と申込者を区別するために、申込者を`taker`と定義することにします。
投稿のモデル名を`Post`とすると、post_takersという中間テーブルを作れば良いということになります。

中間テーブルを作成するにはまず、ターミナルに以下のコマンドを打ちモデルを作成します。

```
$ rails g model PostTaker
```

出来上がったマイグレーションファイルを開き、以下のように記述します。

```2020XXXXXXXXXX_create_post_takers.rb
class CreatePostTakers < ActiveRecord::Migration[6.0]
  def change
    create_table :post_takers do |t|
      t.references :post, foreign_key: true
      t.references :taker, foreign_key: {to_table: :users} 
      t.timestamps
    end
  end
end
```
ここでのポイントは、`t.references :taker, foreign_key: {to_table: :users} `の部分です。
このように書くと、中間テーブルの外部キーとして申込者である`taker`という名前を定義しつつ、usersテーブルを参照できます。
参考記事：https://qiita.com/publichtml/items/1fba15d8071fab66d043

## モデル同士の関連付け（アソシエーション）を行う
次に、モデルにアソシエーションを記述します。

```user.rb
class User < ApplicationRecord
  has_many :post_takers, foreign_key: "taker_id", dependent: :destroy
end
```

```post.rb
class Post < ApplicationRecord
  has_many :post_takers, dependent: :destroy
  has_many :takers, through: :post_takers, dependent: :destroy
end
```

```post_taker.rb
class PostTaker < ApplicationRecord
  belongs_to :taker, class_name: 'User', foreign_key: 'taker_id'
  validates_uniqueness_of :post_id, scope: :taker_id
end
```

post_taker.rbの`belongs_to :taker, class_name: 'User', foreign_key: 'taker_id'`は、
このような書き方をすることでUserモデルに紐付いた申込者と関連付けができます。
参考記事：https://qiita.com/gyu_outputs/items/421cc1cd2eb5b39e20ad

また、`validates_uniqueness_of :post_id, scope: :taker_id`は、
投稿と申込者の同じ組み合わせが2つ以上登録されないようにするため、このようなバリデーションを記述しています。


## ルーティングを定義する
申し込むというアクションを`take`、キャンセルというアクションを`cancel`としてルーティングに以下のように記述します。

```routes.rb
Rails.application.routes.draw do
  resources :posts do
    member do
      get 'take'
      get 'cancel'
    end
  end
end
```
上記では、パスに投稿のidを含めるためmemberを使っています。
参考記事：https://qiita.com/hirokihello/items/fa82863ab10a3052d2ff



## コントローラーに申し込み機能とキャンセル機能を定義する

コントローラーに以下の記述を追加します。
※ここでは投稿詳細ページに申込ボタンを表示させることを前提として書いていますが、各々の状況に合わせて書き換えてください。

```posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post

  def show
    @user = @post.user
  end

  def take
    # 該当の投稿とログイン中のユーザーとの中間テーブルのレコードを作成する
    PostTaker.create(post_id: @post.id, taker_id: current_user.id)
    # フラッシュメッセージを表示（フラッシュメッセージを表示させない場合は書かなくて大丈夫です）
    flash[:notice] = '申し込みが完了しました。'
    # 投稿の詳細ページにリダイレクト
    redirect_to action: "show"
  end

  def cancel
    # 中間テーブルの中から、該当の投稿とログイン中のユーザーによるレコードを抽出する
    post_taker = PostTaker.find_by(post_g_id: @post.id, taker_id: current_user.id)
    post_taker.destroy
    flash[:notice] = 'キャンセルが完了しました。'
    redirect_to action: "show"
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end
end
```

## 申込ボタンのビューを作成する
最後に、申込ボタンのビューを作成したら完成です！
以下のコードはhamlで記述しています。

```show.html.haml
-# 投稿者とログイン中のユーザーが一致しない場合のみボタンを表示させる
- if @user.id != current_user.id
  -# 該当の投稿の申込者の中に、ログイン中のユーザーが含まれているか否かで場合分けする
  - if @post.taker_ids.include?(current_user.id)
    = link_to "申し込みをキャンセルする", cancel_posts_path(@post.id), data: { confirm: "こちらの投稿の申し込みをキャンセルしますか？" }
  - else
    = link_to "申し込む", take_posts_path(@post.id), data: { confirm: "こちらの投稿に申し込みますか？" }
```