【Rails】多対多のアソシエーションを用いてコミュニティ加入の申請/承認機能の実装を行う

# 実施したいこと
申請・承認の仕組みを実装する

# 具体的には？
「ユーザ（小西）が、とあるコミュニティに加入したいと思いますが、
加入にはコミュニティの管理者（大西）の承認が必要になる」といったケースでの実装例です。

# ざっくりイメージ
<img width="413" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590791/df4776f0-650c-9eb3-915b-3f66d67b3734.png">

# 処理フロー概要
1. 該当コミュニティ画面にて、ユーザ（小西）が「申請」ボタンを押す。
2. 該当コミュニティの申請待ちリストにユーザ（小西）が追加される。管理者が、該当コミュニティの申請待ち一覧画面を見て、申請者を承認するかどうか判断。
3. 申請を承認する場合は、承認ボタンを押す。申請ユーザがコミュニティに加入し、申請待ちリストからは消去される。
4. 申請を却下する場合は、否認ボタンを押す。申請待ちリストから消去される。

# 環境
Ruby 2.6.5
Rails 5.2.4.2
mysql 5.7

# 現在のアプリケーション機能の構成
ユーザが色々なコミュニティに所属できるアプリケーションを作成しています。
- userモデル：アプリの利用ユーザ情報を保持
- communityモデル：コミュニティ情報を保持
- belongingモデル：ユーザが所属するコミュニティ情報を保持。ユーザは複数のコミュニティに所属できます。
※userとcommunityはbelongingモデルを介して多対多の関係となっています。

以下ソースは関連部分のみ抜粋

```user.rb
class User < ApplicationRecord
  has_many :belongings, dependent: :destroy
  has_many :applies, dependent: :destroy
  has_many :communities, through: :belongings # ユーザが所属しているコミュニティ

end

```

```community.rb
class Community < ApplicationRecord
    has_many :belongings, dependent: :destroy
    has_many :applies, dependent: :destroy
    has_many :users, through: :belongings # コミュニティに所属しているユーザ

    # ユーザがコミュニティに所属していればtrueを返す
    def user_belonging?(user)
      users.include?(user)
    end

end

```

```belonging.rb
class Belonging < ApplicationRecord
    belongs_to :user
    belongs_to :community

    validates :user_id, presence: true
    validates :community_id, presence: true
    validates  :user_id, uniqueness: { scope: :community_id }
    validates  :community_id, uniqueness: { scope: :user_id }

end
```

# Applyモデルを追加し、申請状況を保持させる。

Applyモデルのルーティングやコントローラ、ビューファイルを追加します。

```apply.rb
class Apply < ApplicationRecord
    belongs_to :user
    belongs_to :community
    validates :user_id, presence: true
    validates :community_id, presence: true
    validates  :user_id, uniqueness: { scope: :community_id}
    validates  :community_id, uniqueness: { scope: :user_id}
end
```

# ルーティング（抜粋）


```routes.rb
Rails.application.routes.draw do
  root 'home#index'

  resources :communities do
    resources :applies, only: %i[index create destroy]
    resources :belongings, only: %i[index create destroy]
  end

  resources :users, only: [:index, :show]

end
```

# 1.該当コミュニティ画面にて、ユーザ（小西）が「申請」ボタンを押す。

- 申請ボタン（コミュニティ詳細画面に配置）
（views/communities/show.html.erb）

```show.html.erb
<!-- ログインユーザが当該コミュニティに所属している場合 -->
<% if @community.user_belonging?(current_user) %>
    <%= link_to '退会する', community_belonging_path(@community, @belonging), method: :delete, data:{ confirm: "コミュニティ「#{@community.name}」を退会します。よろしいですか？" } ,class:"mini-red-link-btn font-bold text-line-none" %>
<!-- 当該コミュニティには所属していないが、ログインはしている場合 -->
<% elsif current_user %>
    <% if @apply %>
        <%= link_to '申請取消', community_apply_path(@community, @apply), method: :delete, class: "mini-red-link-btn font-bold text-line-none" %>
    <% else %>
        <%= link_to '加入申請', community_applies_path(@community), method: :post, class: "mini-green-link-btn font-bold text-line-none" %>
    <% end %>
```

申請ボタンは、以下の条件で表示させます。
1. コミュニティにすでに加入している場合：退会するボタンが表示
2. コミュニティにまだ加入していない場合：申請ボタンが表示
3. すでに加入申請済の場合：申請取消ボタンを表示

- 申請ボタン押下後（createアクションが発生）

```applies_controller.rb
class AppliesController < ApplicationController

  def create
    current_user.applies.create(community_id: apply_params[:community_id])
    redirect_to community_url(apply_params[:community_id]), notice: "加入申請しました"
  end

  private

    def apply_params
      params.permit(:community_id)
    end

end
```

Ajaxは使わず、同画面へのリダイレクトが発生するようにします。
（Ajaxを使ってもよさそう）

**これで、applyモデルに申請情報が追加されました。**

# 2.該当コミュニティの申請待ちリストにユーザ（小西）が追加される。管理者が、該当コミュニティの申請待ち一覧画面を見て、申請者を承認するかどうか判断。

次に、申請待ちリストの確認です。
管理者（大西）が該当コミュニティ画面から「申請待ち一覧画面」を開きます。

- 申請待ち画面へのリンク（コミュニティ詳細画面）
（views/communities/show.html.erb）

```show.html.erb
<% if user_admin_flg(current_user,@community) == 1 %>
    <%= link_to "承認待ち一覧", community_applies_path(@community), class:"btn btn-primary" %>
<% end %>
```

リンクをクリックすると、申請待ち一覧画面が開きます。

- 申請待ち一覧画面
（views/applies/index.html.erb）

```index.html.erb
<div class="container applicant-wrapper">
    <h3>承認待ちユーザ一覧</h3>
    <div class="row">
        <% @applies.each do |app| %>
        <div class="col-6">
            <% if app.user.image.attached? %>
                <%= link_to app.user.image, user_path(app.user), class:"user-icon" %>
            <% else %>
                <%= link_to user_path(app.user) do %>
                    <%= image_tag ("no_image.png"), class:"user-icon" %>
                <% end %>
            <% end %>
            &nbsp<%= app.user.username %><br>
            <%= link_to "承認", community_belongings_path(app.community, user_id: app.user.id, apply_id: app.id), method: :post, class:"mini-green-link-btn font-bold text-line-none" %>
            <%= link_to "却下", community_apply_path(app.community, app), method: :delete, class:"mini-red-link-btn font-bold text-line-none" %>
            <br>
        </div>
        <% end %>
    </div>
</div>
```

applyモデルから、該当コミュニティへの申請情報を一覧表示させています。
同画面において、承認ボタン、却下ボタンも配置しています。

# 3.申請を承認する場合は、承認ボタンを押す。申請ユーザがコミュニティに加入し、申請待ちリストからは消去される。

承認ボタンを押すと、以下の処理が実行されます。
1. belongingsコントローラのcreateアクションが実行される。
2. 申請待ち一覧から削除するため、Applyモデルの該当の申請情報が削除される。
3. 申請待ち一覧画面へリダイレクト

- belongingsコントローラ

```belongings_controller.rb
class BelongingsController < ApplicationController

    def create
        @belonging = Belonging.create(community_id: belonging_params[:community_id], user_id: belonging_params[:user_id])
        Apply.find(belonging_params[:apply_id]).destroy!
        redirect_to community_applies_url(@belonging.community), notice:"「#{@belonging.user.username}」が、コミュニティ：#{@belonging.community.name}へ加入しました。"
    end

    private

        def belonging_params
            params.permit(:community_id, :user_id, :apply_id)
        end

end
```


# 4.申請を却下する場合は、否認ボタンを押す。申請待ちリストから消去される。

却下ボタンを押すと、以下の処理が実行されます。
1. 申請待ち一覧から削除するため、appliesコントローラのdestroyアクションが実行され、Applyモデルの該当の申請情報が削除される。
2. 申請待ち一覧画面へリダイレクト

- appliesコントローラ

```applies_controller.rb
class AppliesController < ApplicationController

  def destroy
    @apply = Apply.find(params[:id])
    @apply.destroy!
    @comminity = Community.find(params[:community_id])
    redirect_to community_url(@comminity), notice: "加入申請を取り消しました"
  end

end
```

以上になります。


文中でappliesコントローラとbelongingsコントローラはアクション毎に分けて紹介していますが、最後に全部まとめて載せておきます。

```applies_controller.rb
class AppliesController < ApplicationController

  def create
    current_user.applies.create(community_id: apply_params[:community_id])
    redirect_to community_url(apply_params[:community_id]), notice: "加入申請しました"
  end

  def destroy
    @apply = Apply.find(params[:id])
    @apply.destroy!
    @comminity = Community.find(params[:community_id])
    redirect_to community_url(@comminity), notice: "加入申請を取り消しました"
  end

  def index
    @applies = Apply.where(community_id: params[:community_id])
  end

  private

    def apply_params
      params.permit(:community_id)
    end

end
```

```belongings_controller.rb
class BelongingsController < ApplicationController

    def create
        @belonging = Belonging.create(community_id: belonging_params[:community_id], user_id: belonging_params[:user_id])
        Apply.find(belonging_params[:apply_id]).destroy!
        redirect_to community_applies_url(@belonging.community), notice:"「#{@belonging.user.username}」が、コミュニティ：#{@belonging.community.name}へ加入しました。"
    end

    def destroy
        @belonging = Belonging.find(params[:id])
        @belonging.destroy!
        @comminity = Community.find(params[:community_id])
        redirect_to community_url(@comminity), notice: "コミュニティ「#{@comminity.name}」を退会しました。"    
    end

    private

        def belonging_params
            params.permit(:community_id, :user_id, :apply_id)
        end

end
```

```communities_controller.rb
class CommunitiesController < ApplicationController
  skip_before_action :authenticate_user!, only: %i[index show]
  before_action :validate_community, only: %i[edit update destroy]

  def index
    # 検索オブジェクト
    @search = Community.ransack(params[:q])
    # 検索結果
    @communities = @search.result
    @communities = @communities.page(params[:page]).per(12)
  end

  def show
    @community = Community.find(params[:id])
    @belongings = Belonging.where(community_id: @community.id)
    @code = Code.all
    # 検索オブジェクト
    @search = Post.ransack(params[:q])
    # 検索結果
    @posts = @search.result
    @posts = @posts.where(community_id: @community.id)
    @posts = @posts.page(params[:page]).per(9)
    return unless current_user

    @belonging = Belonging.find_by(community_id: @community.id, user_id: current_user.id)
    @apply = Apply.find_by(community_id: @community.id, user_id: current_user.id)
  end

  def new
    @community = Community.new
    @code = Code.all
  end

  def edit
    @community = Community.find(params[:id])
    @code = Code.all
  end

  def create
    @community = current_user.communities.build(community_params)

    if @community.save
      Belonging.create!(community_id: @community.id, user_id: current_user.id, admin_flg: '1')
      redirect_to communities_url, notice: "グループ「#{@community.name}」を登録しました。"
    else
      render :new
    end
  end

  def update
    community = current_user.communities.find(params[:id])
    community.update!(community_params)
    redirect_to community_url(community), notice: "グループ「#{community.name}」を更新しました。"
  end

  def destroy
    community = Community.find(params[:id])
    community.destroy
    redirect_to communities_url, notice: "グループ「#{community.name}」を削除しました。"
  end

  private

  def community_params
    params.require(:community).permit(:name, :create_user_id, :publish_flg, :description, :image)
  end

  def validate_community
    @community = Community.find(params[:id])
    redirect_to community_path(@community), alert: "作成ユーザのみ変更操作ができます" if @community.create_user_id != current_user.id
  end
end
```


初心者が自分の頭でロジック・コードを考えて実装したので、
間違いや、もっと適した実装方法があるかもしれません。
ぜひご指摘いただけますと幸いです。

この実装で、多対多の関係のことがより理解できたなと思います。