Rails で namespace を利用している際 resources をネストする方法 ( scaffold 編 )

## やりたい事

* 管理画面用に namespace を利用している ( admin ) 。

* stores ( 店舗情報 ) は、配下に reviews ( レビュー情報 ) を持っているような親子関係を URL でも表現したい。
( /admin/stores/1/reiews )

* また、URL が長くなると見通しが悪くなるため、reviews の id を受け取るアクション ( show, edit, update, destroy ) では url をネストしないようにしたい。
( /admin/reviews/1 )


## 前提

||バージョン|
|:--|:--|
|Ruby|2.1.1|
|Rails|4.2.0|


**stores ( 店舗情報 )**

|列名|データ型|概要|
|:--|:--|:--|
|name|string|店舗名|
|address|string|所在地|
|access|text|アクセス|

**reviews ( レビュー情報 )**

|列名|データ型|概要|
|:--|:--|:--|
|store_id|references|stores テーブルへの外部キー|
|body|text|本文|


## 1. scaffold CRUD 作成

下記の記事を参考に、scaffold で stores の CRUD を作成し、admin という namespace を適用しておく。

[Rails で namespace を適用する方法 ( scaffold 編 )](http://qiita.com/ryo-ichikawa/items/2a9159b489a7b16c3dc6)

同じように scaffold で reviews の CRUD も作成し、namespace を適用しておく。

※ reviews は stores への外部キーを持っているので、generate コマンドは以下のようになる。 

`rails generate scaffold reviews store:references body:text`

この時点では、reviews は stores の外部キーを持っているものの、URL 的な親子関係はない。


## 2. ルーティング修正

ルーティングを以下のように修正する。
shallow オプションを適用する事で、reviews の id を受け取るアクション ( show, edit, update, destroy ) では url がネストされなくなる。

```config/routes.rb
namespace :admin do
  resources :stores do
    resources :reviews, shallow: true
  end
end
```


## 3. アソシエーションの設定

```app/models/store.rb
has_many :reviews
```

```app/models/review.rb
belongs_to :store
```


## 4. reviews の各ソースの修正


### 一覧表示機能のソース修正

reviews コントローラの index アクション修正

```app/controllers/admin/reviews_controller.rb
def index
  store = Store.find(params[:store_id])
  @reviews = store.try(:reviews)
end
```

一覧表示画面で store モデルのインスタンスをそのまま表示するようになってしまっているので修正。

```app/views/admin/reviews/index.html.erb
<tbody>
  <% @reviews.each do |review| %>
    <tr>
      <td><%= review.store.name %></td>
```

一覧表示画面のリンクを修正。

```app/views/admin/reviews/index.html.erb
<td><%= link_to 'Show', [:admin, review] %></td>
<td><%= link_to 'Edit', edit_admin_review_path(review) %></td>
<td><%= link_to 'Destroy', [:admin, review], method: :delete, data: { confirm: 'Are you sure?' } %></td>

・
・
・

<%= link_to 'New Review', new_admin_store_review_path %>
```

---

### 新規登録機能のソース修正

reviews コントローラの new アクション内で、Store モデルを取得。

```app/controllers/admin/reviews_controller.rb
def new
  @store = Store.find(params[:store_id])
  if @store
    @review = @store.reviews.build
  else
    @review = Review.new
  end
end
```

下記、stackoverflow の記事を参考に shallow オプションを適用している際に form_for ヘルパーへ渡す引数を生成するための shallow_args ヘルパーを作成。

[stackoverflow](http://stackoverflow.com/questions/9772588/ "stackoverflow")

```app/helpers/application_helper.rb
def shallow_args(namespace, parent, child)
  if namespace.nil?
    child.try(:new_record?) ? [parent, child] : child
  else
    child.try(:new_record?) ? [namespace, parent, child] : [namespace, child]
  end
end
```

作成した shallow_args ヘルパーを form_for へ適用。

```app/views/admin/reviews/_form.html.erb
<%= form_for shallow_args(:admin, @store, @review) do |f| %>
```

また、store_id は登録、編集できないようにする。

```app/views/admin/reviews/_form.html.erb
<div class="field">
  <%= f.label :store_id %><br>
  <%= @store.name %>
  <%= f.hidden_field :store_id, value: @store.id %>
</div>
```

新規登録画面の「戻る」リンクを修正。

```app/views/admin/reviews/new.html.erb
<%= link_to 'Back', admin_store_reviews_path %>
```

コントローラの create アクション修正

```app/controllers/admin/reviews_controller.rb
if @review.save
  format.html { redirect_to @review, notice: 'Review was successfully created.' }
  format.json { render :show, status: :created, location: @review }

↓以下のように修正

if @review.save
  format.html { redirect_to [:admin, @review], notice: 'Review was successfully created.' }
  format.json { render :show, status: :created, location: [:admin, @review] }
```

---

### 詳細表示機能のソース修正

詳細表示画面で store モデルのインスタンスをそのまま表示するようになってしまっているので修正。

```app/views/admin/reviews/show.html.erb
<p>
  <strong>Store:</strong>
  <%= @review.store.name %>
</p>
```

詳細表示画面のリンクを修正。

```app/views/admin/reviews/show.html.erb
<%= link_to 'Edit', edit_admin_review_path(@review) %> |
<%= link_to 'Back', admin_store_reviews_path(@review.store_id) %>
```

---

### 編集機能のソース修正

reviews コントローラの edit アクション内で、Store モデルを取得。

```app/controllers/admin/reviews_controller.rb
def edit
  @store = @review.store
end
```

reviews コントローラの update アクション修正。

```app/controllers/admin/reviews_controller.rb
if @review.update(review_params)
  format.html { redirect_to [:admin, @review], notice: 'Review was successfully updated.' }
  format.json { render :show, status: :ok, location: [:admin, @review] }
```

編集画面の「戻る」リンクを修正。

```app/views/admin/reviews/edit.html.erb
<%= link_to 'Show', [:admin, @review] %> |
<%= link_to 'Back', admin_store_reviews_path(@store) %>
```

---

### 削除機能のソース修正

reviews コントローラの destroy アクション、redirect_to メソッドの引数を修正。

```app/controllers/admin/reviews_controller.rb
format.html { redirect_to admin_store_reviews_url(@review.store_id), notice: 'Review was successfully destroyed.' }
```

---

### コントローラテストのソース修正

fixtures を修正。
外部キーの部分を モデル名:参照先のラベル 形式へ修正

```test/fixtures/reviews.yml
one:
  store: one 
  body: MyText

two:
  store: two
  body: MyText
```

テストクラスの setup メソッド内で、fixtures/stores.yml をもとに、Store モデルを取得。

```test/controllers/admin/reviews_controller_test.rb
setup do
  @store = stores(:one)
  @review = reviews(:one)
end
```

テストクラス、should get index テストの get メソッドへ引数追加。

```test/controllers/admin/reviews_controller_test.rb
test "should get index" do
  get :index, store_id: @store
```

テストクラス、should get new テストの get メソッドへ引数追加。

```test/controllers/admin/reviews_controller_test.rb
test "should get new" do
  get :new, store_id: @store
```

テストクラス、should create review テストの post メソッドへ引数追加。

```test/controllers/admin/reviews_controller_test.rb
test "should create review" do
  assert_difference('Review.count') do
    post :create, review: { body: @review.body, store_id: @review.store_id }, store_id: @store
  end
```

テストクラス、should destroy review テストの url ヘルパー修正。

```test/controllers/admin/reviews_controller_test.rb
assert_redirected_to admin_store_reviews_path(assigns(:review))
```


## 5. テスト

`$ rake db:migrate`
`$ rake db:test:load`
`$ rake test:controllers TEST="test/controllers/admin/reviews_controller_test.rb"`


エラーが無ければ OK。

$ rake db:test:purge

---

## 6. おまけ

stores の一覧表示画面から reviews の一覧表示画面へリンクできるように、以下を設置しておくと便利。

```app/views/admin/stores/index.html.erb
<td><%= link_to 'Show Review', admin_store_reviews_path(store) %></td>
```

---

お疲れ様でした。