基礎Ruby on Rails #26(最終回) Chapter15 名前空間

前 [基礎Ruby on Rails #25 Chapter14 多対多の関連付け](https://qiita.com/tseno/items/d5856e1ec33e51109d0a)
次 終了

# 名前空間付きのルーティングとコントローラ

## 名前空間を導入する理由

- 一般会員、管理者で、ユーザー種類ごとに別々のコントローラを用意する。
- Admin::MembersControllerのようなコントローラ作成する。このとき、`Admin`はモジュール名。
- controllersのイメージ
    - admin
        - top_controller・・・ 管理者用トップページ
        - members_controller・・・会員管理ページ
        - articles_controller・・・記事管理ページ
    - top_controller・・・一般用トップページ
    - members_controller・・・会員表示ページ
    - articles_controller・・・記事表示ページ

## 管理TOPページへのルーティング設定

- `/admin`→`top#index`へのルーティングを設定する。

```rb:config/routes.rb（一部）
  namespace :admin do
    root "top#index"
  end
```

- 上記パスに飛ぶリンクを作成する方法は以下の通り
    - `link_to "管理ページ", :admin_root`
    - `link_to "管理ページ", [:admin, :root]`

## Admin::Baseクラス

- 全ての管理者用クラスの親クラス`Admin::Base`を作成する。
- 全てのこのクラスの継承クラスで認証を行うためのメソッド`admin_login_required`を追加する。エラーはForbidden。

```rb:app/controllers/admin/base.rb
class Admin::Base < ApplicationController
  before_action :admin_login_required

  private def admin_login_required
    raise Forbidden unless current_member&.administrator?
  end
end
```

## Admin::TopController

- Admin::TopControllerと、indexを作成する。

```sh
$ bin/rails g controller admin/top index
      create  app/controllers/admin/top_controller.rb
      invoke  erb
      create    app/views/admin/top
      create    app/views/admin/top/index.html.erb
```

- 継承元親クラス`ApplicationController`を`Admin::Base`に書き換える。

```rb:app/controllers/admin/top_controller.rb
class Admin::TopController < Admin::Base
  def index
  end
end
```

- indexのビューを作成する。

```erb:app/views/admin/top/index.html.erb
<% @page_title = "管理ページトップ" %>
<ul>
  <li><%= link_to "会員管理", "#" %></li>
  <li><%= link_to "ニュース記事管理", "#" %></li>
</ul>
```

- 管理用なのか一目でわかるようにメニューバーを切り替える。
- 一般用のメニューバーである新規ファイル_menubar.html.erbを作成する。

```erb:app/views/shared/_menubar.html.erb
<nav class="menubar">
  <ul>
    <%= menu_link_to "TOP", :root %>
    <%= menu_link_to "ニュース", :articles %>
    <%= menu_link_to "ブログ", :entries %>
    <% if current_member %>
      <%= menu_link_to "会員名簿", :members %>
      <% if current_member.administrator? %>
        <%= menu_link_to "管理ページ", :admin_root %>
      <% end %>
    <% end %>
  </ul>
</nav>
```

- 管理者用のメニューバーを新規作成する。

```erb:app/views/shared/_admin_menubar.html.erb
<nav class="menubar" id="admin-menubar">
  <ul>
    <%= menu_link_to "管理TOP", :admin_root %>
    <%= menu_link_to "会員管理", "#" %>
    <%= menu_link_to "ニュース記事管理", "#" %>
    <%= menu_link_to "TOP", :root %>
  </ul>
</nav>
```

- コントローラの種類によって、メニューバーを切り替える。

```erb:app/views/shared/_header.html.erb（一部）
<%=
  if controller.kind_of?(Admin::Base)
    render "shared/admin_menubar"
  else
    render "shared/menubar"
  end
%>
```

- 管理者用のメニューバーの色を変えるために、CSSを追加する。

```css:app/assets/stylesheets/admin.css
nav#admin-menubar {
  background-color: #800;
}
```

- 管理者用のメニューバーは赤く表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/37fc2429-a0f3-1793-0a4f-0434f51d30cd.png)

- Jiroでアクセスすると403エラーになることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/b8f532a7-7f07-a607-6e34-c4b993ebadb5.png)

# 会員管理ページの作成

## ルーティング設定

- MembersControllerではindexとshow以外のアクションを廃止するために、`only: [:index, :show]`を追加する。

```rb:config/routes.rb（一部）
  resources :members, only: [:index, :show] do
    get "search", on: :collection
    resources :entries, only: [:index]
  end
```

- Admin::MembersControllerへのルーティングを追加する。

```rb:config/routes.rb（一部）
  namespace :admin do
    root "top#index"
    resource :members do     # 以下を追加する
      get "search", on: :collection
    end
  end
```

- 以上の設定変更により、新たなルーティングが次のように設定される。

|アクション|パス|HTTPメソッド|パスを返すメソッド|
|:--|:--|:--|:--|
|index|/admin/members|GET|admin_members_path|
|show|/admin/members/123|GET|admin_members_path(member)|
|new|/admin/members/new|GET|new_admin_member_path|
|edit|/admin/members/123/edit|GET|edit_admin_members_path(member)|
|create|/admin/members|POST|admin_members_path|
|update|/admin/members/123|PATCH|admin_members_path(member)|
|destroy|/admin/members/123|DELETE|admin_members_path(member)|
|search|/admin/members/search|GET|search_admin_members_path|

- 管理者向けメニューバーのテンプレートに会員管理ページへのリンクを追加する。

```erb:app/views/shared/_admin_menubar.html.erb（一部）
    <%= menu_link_to "会員管理", :admin_members %>
```

- 管理トップページのテンプレートにも会員管理ページへのリンクを追加する。

```erb:app/views/shared/_admin_menubar.html.erb（一部）
  <li><%= link_to "会員管理", :admin_members %></li>
```

## Admin::MembersControllerの作成

- 今までのmembersのcontrollerとviewのファイルを全てadminにコピーする。
    - app/controllersディレクトリの下のmembers_controller.rbをコピーして、app/controllers/adminディレクリにコピーする
    - app/views/membersディレクトリをまるごとコピーして、app/views/adminディレクトリの下に貼り付ける。
#
- MembersControllerに名前空間と親クラスを変更して、before_actionを削除する。
- create、update、destroyアクションの中にあるリダイレクト先を名前空間付きのものに変更する。

```rb:app/controllers/admin/members_controller.rb（一部）
#変更前
class MembersController < ApplicationController
  before_action :login_required
#変更後
class Admin::MembersController < Admin::Base

# create
      redirect_to [:admin, @member], notice: "会員を登録しました。"
# update
      redirect_to [:admin, @member], notice: "会員を更新しました。"
# destroy
    redirect_to :admin_members, notice: "会員を削除しました。"
```

## 会員管理ページ用のテンプレート修正

- パスの指定を名前空間付きのものに変更する。
- アクションのシンボルが名前空間を表すシンボルよりも前に来る点に注意すること。

```erb:app/views/admin/members/index.html.erb（一部）
<!-- 変更前 -->
<% @page_title = "会員名簿" %>
<!-- 変更後 -->
<% @page_title = "会員管理" %>

<!-- 変更前 -->
<div class="toolbar"><%= link_to "会員の新規登録", :new_member %></div>
<!-- 変更後 -->
<div class="toolbar"><%= link_to "会員の新規登録", :new_admin_member %></div>

<!-- 変更前 -->
        <td><%= link_to member.name, member %></td>
<!-- 変更後 -->
        <td><%= link_to member.name, [:admin, member] %></td>

<!-- 変更前 -->
        <td><%= link_to "編集", [:edit, member] %>｜
          <%= link_to "削除", member, method: :delete, data:{ confirm: "☺本当に削除しますか❓" } %></td>
<!-- 変更後 -->
        <td><%= link_to "編集", [:edit, :admin, member] %>｜
          <%= link_to "削除", [:admin, member], method: :delete, data:{ confirm: "☺本当に削除しますか❓" } %></td>
```

```erb:app/views/admin/members/show.html.erb（一部）
<!-- 変更前 -->
<div class="toolbar"><%= link_to "編集", [:edit, @member] %></div>
<!-- 変更後 -->
<div class="toolbar"><%= link_to "編集", [:edit, :admin, @member] %></div>
```

- 一般向けのshowアクションのテンプレートでは「編集」リンク自体を削る。

```erb:app/views/members/show.html.erb
<% @page_title = "会員の詳細" %>

<h1><%= @page_title %></h1>

<!-- 削除 <div class="toolbar"><%= link_to "編集", [:edit, @member] %></div> -->

<%= render "body" %>
```

- adminのnew、editアクションでは、リンクに:adminを追加する。

```erb:app/views/admin/members/new.html.erb
<!-- 変更前 -->
<%= form_for @member do |form| %>
<!-- 変更後 -->
<%= form_for [:admin, @member] do |form| %>
```

- adminのnew、editアクションでは、リンクに:adminを追加する。

```erb:app/views/admin/members/edit.html.erb
<!-- 変更前 -->
<div class="toolbar"><%= link_to "会員の詳細に戻る", @member %></div>

<%= form_for @member do |form| %>
<!-- 変更後 -->
<div class="toolbar"><%= link_to "会員の詳細に戻る", [:admin, @member] %></div>

<%= form_for [:admin, @member] do |form| %>
```

- adminのnew、editアクションでは、リンクに:adminを追加する。

```erb:app/views/shared/_member_form.html.erb（一部）
<!-- 変更前 -->
  <% if controller.kind_of?(MembersController) %>
<!-- 変更後 -->
  <% if controller.kind_of?(Admin::MembersController) %>
```

- 一覧、編集、削除が動作することを確認した。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/55d414d7-f4ec-5f81-cb85-a2bb625b859a.png)

## MembersControllerの修正

- 一般会員向けのMembersControllerにはindex、show、searchの3つのアクションだけ残す。
    - index、show、search以外のアクションを消す。ストロング・パラメータも消す。
    - viewからnew、editを削除する。
- app/views/members/index.html.erb
    - 一覧の新規登録`<div class="toolbar"><%= link_to "会員の新規登録", :new_member %></div>`を消す。
    - 一覧の操作列を消す。（編集｜削除リンクも消す）

- 一般向け画面では、閲覧しかできないことを確認した。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/9b5ee841-69b1-59d4-be22-ed286a0dec5d.png)

# ニュース記事管理ページの作成

- ニュース記事も会員と同様に管理画面をAdminに持っていく。

## ルーティングの設定

- `resources :articles`に、`only: [:index, :show]`を付加する。
- `namespace :admin do`内に、`resources :articles`を追加する。

```rb:config/routes.rb（一部）
  resources :articles, only: [:index, :show]  # 追加

#（省略）
  namespace :admin do
    root "top#index"
    resources :members do
      get "search", on: :collection
    end
    resources :articles  # 追加
  end
end
```

- 管理者向けメニューバーのテンプレートにニュース記事管理ページへのリンクを追加する。

```rb:app/views/shared/_admin_menubar.html.erb（一部）
    <%= menu_link_to "ニュース記事管理", :admin_articles %>
```

- 管理トップページのテンプレートにもニュース記事管理ページへのリンクを追加する。

```rb:app/views/admin/top/index.html.erb（一部）
  <li><%= link_to "ニュース記事管理", :admin_articles %></li>
```

## Admin::ArticlesControllerの作成

- app/controllersディレクトリの下のarticle_controller.rbをコピーして、app/controller/adminディレクトリの下に貼り付ける。
- app/views/articlesディレクトリをまるごとコピーして、app/views/adminディレクトリの下に貼り付ける。

#

- クラスの定義に名前空間をつける。
- index、show、create、update、destroyアクションを書き換える

```rb:app/controllers/admin/articles_controller.rb（一部）
# 変更前
class ArticlesController < ApplicationController
# 変更後
class Admin::ArticlesController < Admin::Base


# 変更前
  def index
    @articles = Article.order(released_at: :desc)

    @articles = @articles.open_to_the_public unless current_member
    unless current_member&.administrator?
      @articles = @articles.visible
    end

    @articles = @articles.page(params[:page]).per(5)
  end
# 変更後
  def index
    @articles = Article.order(released_at: :desc).page(params[:page]).per(5)
  end


# 変更前
  def show
    articles = Article.all

    articles = articles.open_to_the_public unless current_member

    unless current_member&.administrator?
      articles = articles.visible
    end
    @article = articles.find(params[:id])
  end
# 変更後
  def show
    @article = Article.find(params[:id])
  end

# 変更前 create、update、destroy
      redirect_to @article, notice: "ニュース記事を登録しました。"
      redirect_to @article, notice: "ニュース記事を更新しました。"
    redirect_to :articles, notice: "ニュース記事を削除しました。"
# 変更後
      redirect_to [:admin, @article], notice: "ニュース記事を登録しました。"
      redirect_to [:admin, @article], notice: "ニュース記事を更新しました。"
    redirect_to :admin_articles, notice: "ニュース記事を削除しました。"
```

## 記事管理用のテンプレート修正

- indexテンプレートの書き換え

```erb:app/views/admin/articles/index.html.erb（一部）
<!-- 変更前 -->
<div class="toolbar"><%= link_to "新規作成", :new_article %></div>
<!-- 省略 -->
          <td><%= link_to article.title, article %></td>
<!-- 省略 -->
            <%= link_to "編集", [:edit, article] %> |
            <%= link_to "削除", article, method: :delete, data: { confirm: "本当に削除しますか？" } %>
<!-- 変更後 -->
<div class="toolbar"><%= link_to "新規作成", :new_admin_article %></div>
<!-- 省略 -->
        <td><%= link_to article.title, [:admin, article] %></td>
<!-- 省略 -->
          <%= link_to "編集", [:edit, :admin, article] %> |
          <%= link_to "削除", [:admin, article], method: :delete, data: {confirm: "本当に削除しますか？"} %>
```

- showテンプレートの書き換え

```erb:app/views/admin/articles/show.html.erb（一部）
<!-- 変更前 -->
  <div class="toolbar"><%= link_to "編集", [:edit, @article] %></div>
<!-- 変更後 -->
  <div class="toolbar"><%= link_to "編集", [:edit, :admin, @article] %></div>
```

- newテンプレートの書き換え

```erb:app/views/admin/articles/new.html.erb（一部）
<!-- 変更前 -->
<%= form_for @article do |form| %>
<!-- 変更後 -->
<%= form_for [:admin, @article] do |form| %>
```

- editテンプレートの書き換え

```erb:app/views/admin/articles/edit.html.erb（一部）
<!-- 変更前 -->
<p><%= link_to "記事の詳細に戻る", @article %></p>

<%= form_for @article do |form| %>
<!-- 変更後 -->
<p><%= link_to "記事の詳細に戻る", [:admin, @article] %></p>

<%= form_for [:admin, @article] do |form| %>
```

- 管理者向けニュース一覧

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/6bed4e04-4d36-2f0e-d1d6-e8c942d07872.png)




## ArticlesControllerの修正

- 全ユーザー向けのArticlesControllerにはindexアクションとshowアクションだけを残す。
- app/views/articleのnew,edit,_formの3つのビューを削除する。


```rb:app/controllers/articles_controller.rb
# 変更前
class ArticlesController < ApplicationController
  before_action :login_required, except: [:index, :show]

  def index
    @articles = Article.order(released_at: :desc)

    @articles = @articles.open_to_the_public unless current_member
    unless current_member&.administrator?
      @articles = @articles.visible
    end

    @articles = @articles.page(params[:page]).per(5)
  end

  def show
    articles = Article.all

    articles = articles.open_to_the_public unless current_member

    unless current_member&.administrator?
      articles = articles.visible
    end
    @article = articles.find(params[:id])
  end
end


# 変更後
class ArticlesController < ApplicationController
  before_action :login_required, except: [:index, :show]

  def index
    @articles = Article.visible.order(released_at: :desc)
    @articles = @articles.open_to_the_public unless current_member
    @articles = @articles.page(params[:page]).per(5)
  end

  def show
    articles = Article.visible
    articles = articles.open_to_the_public unless current_member
    @article = articles.find(params[:id])
  end
end
```

```erb:app/views/articles/index.html.erb
<% @page_title = "ニュース一覧" %>
<h1><%= @page_title %></h1>

<% if @articles.present? %>
      <% @articles.each do |article| %>
    <h2><%= article.title %></h2>
    <p>
      <%= truncate(article.body, length: 80) %>
      <%= link_to "もっと読む", article %>
    </p>
    <div class="article-footer">
      <%= article.released_at.strftime("%Y/%m/%d %H:%M") %>
    </div>
      <% end %>
<% else %>
  <p>ニュースがありません。</p>
<% end %>
```

```erb:app/views/articles/show.html.erb
<% @page_title = @article.title %>
<h1><%= @article.title %></h1>

<%= simple_format(@article.body) %>

<div class="article-footer">
  <%= @article.released_at.strftime("%Y/%m/%d %H:%M") %>
</div>
```

```css:app/assets/stylesheets/articles.css
div.article-footer {
  border-top: 1px #ccf dashed;
  padding-top: 4px;
  margin-bottom: 8px;
  text-align: right;
  font-size: 75%;
}
```

- 一般向けニュース一覧

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/3f8a10d8-453f-5ef4-aa8a-6a4efddbd993.png)

- 一般向けニュース詳細

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/6899a27f-6320-82e2-55d6-223c7aaabf23.png)


### まとめ

- 管理ページを作るには名前空間付きのコントローラを作成する。
- 最後の方はなかなか難しかったので、整理して理解できるようにしたい。

参考
[改訂4版　基礎Ruby on Rails 基礎シリーズ](https://www.amazon.co.jp/dp/B07GXB2ZSW)