【Rails】同じグループのアクションのルーティングをネストと非ネストに分ける

# はじめに
オリアプに関して、実装時に考えていたことを忘れないように頭の整理目的で書き残したいと思います。  
実装途中の話を公開しますので、見た人がわかるような文章構成になっていない可能性があります(全部語ると長くなり、本来のメイン目的である頭の整理に沿わないため)。
結論だけ知りたい方は「解決方法」だけ見ればOKです。
また、必ずしも時系列ではないです。

# 自分のこと
先日プログラミングスクールを卒業して現在転職活動中です。  
プログラミング未経験の状態からプログラミングスクールに通い始めました。
転職活動と同時並行でオリジナルアプリの開発をしています。

# オリジナルアプリについて
マンション管理アプリを作成しています。  
メイン機能としては分譲マンションの管理会社が社内で使用する(想定の)業務アプリです。  
サブ機能として、マンション住民も一部の機能にアクセスできるようにしています。
## アプリのコンセプト
これを知らないと、この人は何をそんなにこねくり回しているんだとなりそうなので、このアプリのポイントを2つ挙げます。
### ・複数のユーザーモデルが存在すること
deviceを使って管理会社と区分所有者のユーザーモデルを作成しています。
### ・権限管理でユーザー毎に各アクションを制御する
区分所有者や平社員がマンションの情報や登録アクションに全てアクセスできるとマズイので権限管理で制御しています。
## 登場人物
### 管理会社
権限に応じてできることを制限できます。現在は管理会社(user)をadmin、manage、employeeの3種類に分けています。
#### admin

マンション、社員、区分所有者等の登録を含めた全ての操作が可能です。
#### manage

社員の登録以外の操作が可能なアカウントです。役職者やデータ操作をさせる専門部署などに割り当てます。
  
#### employee

基本的に閲覧のみが可能なアカウントです。マンションの担当者であるフロント(営業)はこちらのアカウントを使用します。  

### 区分所有者
#### condo_user
マンション住民も別のユーザモデル(condo_user)として一部のマンション情報にアクセスしたり、管理会社への問い合わせ機能を利用できるようにしています。 


なお、どうでもいいですが、区分所有者とはお部屋を購入して所有権を有する人のことを指します。  
分譲マンションの住民というと、区分所有者だけでなく区分所有者から部屋を借りている賃借人が含まれるようなニュアンスになるのが嫌なので、一般の人には聞き慣れない呼称ですが、区分所有者と呼ばせていただきます。

# 書類(PDF)アップロード機能の実装のときに悩んだこと
## ルーティング通りの画面遷移じゃないと混乱
書類アップロード機能(pdf)の実装時に、書類一覧ページにあった書類新規登録ページへのリンクボタンを、管理会社のログインしたときのトップページに移動したくなった。
```:現在の画面遷移（前者）
トップページ  
↓  
管理会社ログイン  
↓  
社員(employee)トップページ  
↓  
※ヘッダーのプルダウン(担当マンション一覧)からマンションを選択する  
↓
マンショントップページ  
↓  
書類一覧ページ  
↓  
書類登録ページ
```

```:やりたい挙動（後者）
トップページ  
↓  
管理会社ログイン  
↓  
社員(adminとmanage)トップページ  
↓  
※ヘッダーのプルダウンから書類登録ボタンを選択する  
↓
書類登録ページ
```

なぜこんなことがしたいかというと、実務ではemployeeのような社員が直接重要書類のアップロードはせず、稟議を回して専門部署等によって登録されているから。  
つまりこのアプリでいうと、登録行為はadminとmanageのような一部のユーザーにのみにしたい。  
平社員であるemployeeや区分所有者(condo_user)には一覧表示のみアクセスできるようにしたい。  
## 補足
各種登録はadminとmanageに集約したいので、一覧表示ページの周りに新規登録ページがあるような普通の実装よりややこしくなっている。  
つまり見出しに書いた通り、本来Railsが想定しているような前者の画面遷移じゃないと複雑化して混乱する要因になる。

# 現状の記述
前者だと、ルーティングでマンションに書類をネストして以下のような記述にすれば実装できる

```ruby:routes.rb
  resources :condos do
(省略)
    resources :documents, only: [:index, :new, :create]
  end
```

```ruby:app\controllers\documents_controller.rb
class DocumentsController < ApplicationController
  before_action :set_condo

  def index
    @documents = @condo.documents
  end

  def new
    @document = @condo.documents.build
  end

  def create
    @document = @condo.documents.build(document_params)
    @document.user = current_user
  
    if @document.save
      redirect_to @condo, notice: '書類が正常にアップロードされました。'
    else
      render :new
    end
  end

  private

  def set_condo
    @condo = Condo.find(params[:condo_id])
  end

  def document_params
    params.require(:document).permit(:title, :category_id, :file)
  end
end
```
## 個別解説
```ruby:一覧表示
  def index
    @documents = @condo.documents
  end

  private

  def set_condo
    @condo = Condo.find(params[:condo_id])
  end
```
ここでやってることは、before_actionで
- Condo.findでマンションモデルのテーブルから特定のレコードを探す
- params[:condo_id] は、HTTPリクエストを通じて送られてくるパラメーター（この場合はマンションのID）を取得
- インスタンス変数への代入

それを
- @condo.documents というアソシエーションを利用した記述で、そのマンションに関連付けられているすべての書類を取得。
- @documents という新しいインスタンス変数に代入

```ruby:投稿
class DocumentsController < ApplicationController
  before_action :set_condo

  def index
    @documents = @condo.documents
  end

  def new
    @document = @condo.documents.build
  end

  def create
    @document = @condo.documents.build(document_params)
    @document.user = current_user
  
    if @document.save
      redirect_to @condo, notice: '書類が正常にアップロードされました。'
    else
      render :new
    end
  end

  private

  def set_condo
    @condo = Condo.find(params[:condo_id])
  end

  def document_params
    params.require(:document).permit(:title, :category_id, :file)
  end
end
```
newでは
- 空の書類オブジェクト作成(ユーザーが書類に関する情報を入力するためのフォームを表示する際に使用)

Createでは
- ストロングパラメーターを用いて、ユーザーがフォームで入力したデータを受け取り、新しい書類オブジェクトを作成し保存。

# やりたいことと問題点
いままでマンションやら区分所有者やらの新規登録はadminとmanageがログインしたトップページのヘッダープルダウンメニューからアクセスできるようにしているから、書類新規登録もそこにまとめたい。

しかし、現状ルーティングでネストしているので、マンションidが特定出来ていない状態でリンクボタンを設置して押してもマンションIDがありませんとなる。

ネストから外すとなると、今度はCondo.find(params[:condo_id])してるindexが表示できなくなるはず。うーんどうすれば実装できるか。

# 過去実装した類似事例

実は過去似たような実装をやっていたため、書類登録もadminやmanageに限定して、newページでマンションを選択できるように出来るのではないか？という発想になった。

## マンション登録(condo)で担当者(user)を選択する事例
これはcondoの新規登録ページへのリンクをadminとmanageからアクセスできるようにし、登録ページで担当者をプルダウンメニューから選択できるようにしたときの例。
```ruby:config\routes.rb
  resources :condos do
(省略)
  end
```
```erb:app\views\condos\index.html.erb表示時のヘッダーapp\views\layouts\application.html.erb
          <% if current_user.admin? %>
            <%= link_to "管理者画面", admin_root_path, class: "btn btn-success", role: "button" %>
            <div class="btn-group">
              <button type="button" class="btn btn-warning dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
                  <path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"></path>
                </svg>
              </button>
              <ul class="dropdown-menu">
                <%= link_to "区分所有者登録", new_condo_user_registration_path, data: { turbo: false }, class: "dropdown-item" %>
                <%= link_to "社員登録", new_user_registration_path, data: { turbo: false }, class: "dropdown-item" %>
                <%= link_to "マンション登録", new_condo_path, data: { turbo: false }, class: "dropdown-item" %>
              </ul>
            </div>
```
```ruby:app\controllers\condos_controller.rb
  def new
    @condo = Condo.new
    @users_for_select = users_for_select
    @selected_user_id = User.first.id
    authorize @condo
  end
  
  private
  
  def users_for_select
    User.all.map { |user| ["#{user.last_name} #{user.first_name}", user.id] }
  end
```
new アクションでは、新しいマンション（@condo）のインスタンスを作成。  
@users_for_select は、担当者を選択するためのユーザーリストを準備。このリストは、ユーザーの姓と名を組み合わせた文字列と、そのユーザーのIDを含む配列にしている。  
@selected_user_id は、フォームでデフォルトで選択されるユーザーのIDを指定。
```erb:app\views\condos\new.html.erb
<%= form_with(model: @condo, data: { turbo: false }, local: true) do |f| %>

(省略)

  <div class="form-group">
    <div class="form-text-wrap">
      <%= f.label :user_id, '担当者を選択', class: "form-text" %>
      <span class="indispensable">必須</span>
    </div>
    <%= f.select :user_id, options_for_select(
      @users_for_select,
      @selected_user_id),
      { class: "input-default" } %>
  </div>

(省略)
  
```
f.select :user_id の部分で、担当者（user）を選択するためのプルダウンメニューを作成。  
options_for_select メソッドは、選択可能なユーザーのリスト（@users_for_select）と、デフォルトで選択されるユーザー（@selected_user_id）を使用して、プルダウンメニューのオプションを生成。

## 区分所有者登録(condo_user)でマンション(condo)を選択する事例
これはcondo_userの新規登録ページへのリンクをadminとmanageからアクセスできるようにし、登録ページでマンションをプルダウンメニューから選択できるようにしたときの例。

```ruby:config\routes.rb
  devise_for :condo_users, controllers: {
    sessions: 'condo_users/sessions',
    passwords: 'condo_users/passwords',
    registrations: 'condo_users/registrations'
  }
```
```erb:app\views\condos\index.html.erb表示時のヘッダーapp\views\layouts\application.html.erb
          <% if current_user.admin? %>
            <%= link_to "管理者画面", admin_root_path, class: "btn btn-success", role: "button" %>
            <div class="btn-group">
              <button type="button" class="btn btn-warning dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
                  <path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"></path>
                </svg>
              </button>
              <ul class="dropdown-menu">
                <%= link_to "区分所有者登録", new_condo_user_registration_path, data: { turbo: false }, class: "dropdown-item" %>
                <%= link_to "社員登録", new_user_registration_path, data: { turbo: false }, class: "dropdown-item" %>
                <%= link_to "マンション登録", new_condo_path, data: { turbo: false }, class: "dropdown-item" %>
              </ul>
            </div>
```
```ruby:app\controllers\condo_users\registrations_controller.rb
  def new
    @condo_user = CondoUser.new
    @condos_for_select = condos_for_select
    @selected_condo_id = Condo.first.id
    authorize @condo_user
    render 'new'
  end

  private

  def condos_for_select
    Condo.all.map { |condo| [condo.condo_name, condo.id] }
  end
```
new アクションでは、新しい区分所有者（@condo_user）のインスタンスを作成。  
@condos_for_select は、マンションを選択するためのリストを準備。このリストは、マンションの名前とIDを含む配列にしている。
@selected_condo_id は、フォームでデフォルトで選択されるマンションのIDを指定。
```erb:app\views\condo_users\registrations\new.html.erb
<%= form_with model: @condo_user, url: condo_user_registration_path, data: { turbo: false }, class: 'registration-main', local: true do |f| %>

(省略)

<div class="form-group">
  <div class="form-text-wrap">
    <%= f.label :condo_id, 'マンションを選択', class: "form-text" %>
    <span class="indispensable">必須</span>
  </div>
  <%= f.select :condo_id, options_for_select(
    @condos_for_select,
    @selected_condo_id),
    { class: "input-default" } %>
</div>

(省略)

```
f.select :condo_id の部分で、これはマンション（condo）を選択するためのプルダウンメニューを作成。  
options_for_select メソッドは、選択可能なマンションのリスト（@condos_for_select）と、デフォルトで選択されるマンション（@selected_condo_id）を使用して、プルダウンメニューのオプションを生成。

# 類似事例を真似てみる
「区分所有者登録(condo_user)でマンション(condo)を選択する事例」では同じくマンション(condo)を選択するものであるから、こちらを真似る。
```ruby:config\routes.rb
  resources :documents, only: [:index, :new, :create]
```
condoにネストしていたのをやめて独立させる。
```ruby:app\controllers\documents_controller.rb
class DocumentsController < ApplicationController
  before_action :set_condo, only: [:index]

  def index
    @documents = @condo.documents
  end

  def new
    @document = Document.new
    @condos_for_select = condos_for_select
    @selected_condo_id = Condo.first.id
    render 'new'
  end

  def create
    if current_user.admin? || current_user.manage?
      @document = Document.new(document_params)
      @condos_for_select = condos_for_select

      if @document.save
        redirect_to root_path, notice: '書類が正常にアップロードされました。'
      else
        render :new
      end
    end
  end

  private

  def set_condo
    @condo = Condo.find(params[:condo_id])
  end

  def document_params
    params.require(:document).permit(:title, :category_id, :file, :condo_id)
  end

  def condos_for_select
    Condo.all.map { |condo| [condo.condo_name, condo.id] }
  end
end
```
```erb:app\views\documents\new.html.erb
<div class="form-group">
  <div class="form-text-wrap">
    <%= f.label :condo_id, 'マンションを選択', class: "form-text" %>
    <span class="indispensable">必須</span>
  </div>
  <%= f.select :condo_id, options_for_select(
    @condos_for_select,
    @selected_condo_id),
    { class: "input-default" } %>
</div>
```
## 結果
わかってたけど、今度は管理会社adminとmanageからは書類登録できるが、管理会社employeeとマンション住民(condo_user)からの一覧表示へのアクセスは、マンションのIDの特定ができずエラーになる。  

# 類似事例と今回のケースとの違い
今回は、一覧表示か(ネストあり)登録か(ネストなし)の二者択一状態になっている。片方ずつの実装はできるけど両方を実装しようとするとうまくいかない。  
一旦現状の整理。

## ①マンション情報
マンション登録(condo)で担当者(user)を選択するの事例
- ルーティング→独立
- 一覧表示→管理会社employeeと区分所有者(condo_user)
## ②区分所有者情報
区分所有者登録(condo_user)でマンション(condo)を選択する事例
- ルーティング→独立
- 一覧表示→管理会社employeeのみ
## ③書類情報
書類登録(document)でマンション(condo)を選択する事例
- ルーティング→？？？
- 一覧表示→管理会社employeeと区分所有者(condo_user)

| 状態 | 登録 | 一覧表示 |
|------|------|----------|
| ネスト | × | ○ |
| 独立 | ○ | × |

# 解決方法
同じグループのアクションのルーティングをネストと非ネストに分ける事ができるのを知り解決できた。  
エラー解消された時は思わず「おぉー！」といってしまった。笑
```ruby:config\routes.rb
  resources :condos do
(省略)
    resources :documents, only: [:index] # ネストされたルーティング
  end

  resources :documents, only: [:new, :create] # 非ネストされたルーティング
```

# 最後に
機能実装優先で進めていたので、過去の類似事例との比較も出来て頭がスッキリしました。  
上級者からすると、色々突っ込みどころがある内容かもしれませんが、知識がないなりに色々思考を巡らせて工夫していくのが楽しいです！
次は駐輪場と駐車場区画情報を実装したいです。