[Rails] 管理画面にCRUD機能を作成する

学習記録

はじめに

管理画面に、掲示板とユーザのCRUD機能を作成しましょう。

掲示板一覧画面、ユーザー一覧画面には検索機能も実装します。

コントローラの作成

Admin::Boardsコントローラの作成

$ bundle exec rails g controller admin::boards
class Admin::BoardsController < Admin::BaseController
  before_action :set_board, only: %i[show edit update destroy]

  def index
    @search = Board.ransack(params[:q])
    @boards = @search.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end

  def show; end
  
  def edit; end

  def update
    if @board.update(board_params)
      redirect_to admin_board_path(@board), success: t('defaults.message.updated', item: Board.model_name.human)
    else
      flash.now[:danger] = t('defaults.message.not_updated', item: Board.model_name.human)
      render :edit
    end
  end

  def destroy
    @board.destroy!
    redirect_to admin_boards_path, success: t('defaults.message.deleted', item: Board.model_name.human)
  end

  private

  def set_board
    @board = Board.find(params[:id])
  end

  def board_params
    params.require(:board).permit(:title, :body, :board_image, :board_image_cache)
  end
end

Admin::Usersコントローラの作成

$ bundle exec rails g controller admin::users
class Admin::UsersController < Admin::BaseController
  before_action :set_user, only: %i[show edit update destroy]

  def index
    @search = User.ransack(params[:q])
    @users = @search.result(distinct: true).order(created_at: :desc).page(params[:page])
  end
  
  def show; end

  def edit; end

  def update
    if @user.update(user_params)
      redirect_to admin_user_path(@user), success: t('defaults.message.updated', item: User.model_name.human)
    else
      flash.now[:danger] = t('defaults.message.not_updated', item: User.model_name.human)
      render :edit
    end
  end

  def destroy
    @user.destroy!
    redirect_to admin_users_path, success: t('defaults.message.deleted', item: User.model_name.human)
  end


  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:first_name, :last_name, :email, :avatar, :avatar_cache, :role)
  end
end

ルーティング設定

namespace :admin do
  root to: 'dashboards#index'
  get 'login', to: 'user_sessions#new'
  post 'login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy'
  resources :users, only: %i[index show edit update destroy]
  resources :boards, only: %i[index show edit update destroy]
end

ユーザ一覧画面の作成

まずはユーザ一覧画面から作成していきます。

<% content_for(:title, t('.title')) %>
<div class="container mb-5 pt-2">
  <h1><%= t('.title') %></h1>
  <div class="row">
    <div class="col-md-12 mb-3">
      <%= render 'search_form' %>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <table class="table table-striped">
        <thead>
        <tr>
          <th scope="col"><%= User.human_attribute_name(:id) %></th>
          <th scope="col"><%= User.human_attribute_name(:full_name) %></th>
          <th scope="col"><%= User.human_attribute_name(:role) %></th>
          <th scope="col"><%= User.human_attribute_name(:created_at) %></th>
          <th scope="col"></th>
        </tr>
        </thead>
        <tbody>
        <%= render @users %>
        </tbody>
      </table>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <!-- ページネーション -->
      <%= paginate @users %>
    </div>
  </div>
</div>

一覧画面に個々のユーザーを表示するパーシャルを作成

<tr>
  <td>
    <%= user.id %>
  </td>
  <td>
    <%= user.decorate.full_name %>
  </td>
  <td>
    <%= user.role_i18n %>
  </td>
  <td>
    <%= l user.created_at, format: :long %>
  </td>
  <td>
    <%= link_to t('defaults.show'), admin_user_path(user), class: 'btn btn-info' %>
    <%= link_to t('defaults.edit'), edit_admin_user_path(user), class: 'btn btn-success' %>
    <%= link_to t('defaults.delete'), admin_user_path(user), method: :delete, data: { confirm: t('defaults.message.delete_confirm') }, class: 'btn btn-danger' %>
  </td>
</tr>

検索フォームのパーシャルを作成する

検索フォーム部分を部分テンプレート(パーシャル)にします。

<%= search_form_for @search, url: admin_users_path do |f| %>         # admin/users#indexへリクエストを投げる
  <div class="row">
    <div class="form-inline align-items-center mx-auto">
      <div class="col-auto">
        <%= f.search_field :first_name_or_last_name_cont, class: 'form-control', placeholder: t('defaults.search_word') %>        #名前を部分検索
      </div>
      <div class="col-auto">
        <%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>
      </div>
      <div class="col-auto">
        <%= f.submit class: 'btn btn-primary' %>
      </div>
    </div>
  </div>
<% end %>

ここでは、enum_helpというgemと、ransackを使ってプルダウン検索機能を実装しています。やや説明が長くなりそうなので、この部分は別の記事で解説することにしましょう。

サイドメニューにリンクを追加し、アクティブ化

ユーザ一覧画面へ遷移するリンクを追記しましょう。

<%= link_to admin_users_path, class: "nav-link" do %>
  <i class="nav-icon far fa-user"></i>
  <p>
    <%= t('admin.users.index.title') %>
  </p>
<% end %>

こちらもサイドメニューのアクティブ化については別の記事で解説することにします。

ユーザの詳細画面

<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1><%= t('.title') %></h1>
      <div class="text-right mb-3">
        <%= link_to t('defaults.edit'), edit_admin_user_path(@user), class: 'btn btn-success' %>
        <%= link_to t('defaults.delete'), admin_user_path(@user), method: :delete, data: { confirm: t('defaults.message.delete_confirm') }, class: 'btn btn-danger' %>
      </div>
      <table class="table table-bordered bg-white">
        <tr>
          <th scope="row"><%= User.human_attribute_name(:id) %></th>
          <td><%= @user.id %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:role) %></th>
          <td><%= @user.role_i18n %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:full_name) %></th>
          <td><%= @user.decorate.full_name %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:avatar) %></th>
          <td><%= image_tag @user.avatar.url %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:created_at) %></th>
          <td><%= l @user.created_at, format: :long %></td>
        </tr>
      </table>
    </div>
  </div>
</div>

ユーザの編集画面

<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1><%= t '.title' %></h1>
      <%= form_with model: @user, url: admin_user_path(@user), local: true do |f| %>
        <%= render 'shared/error_messages', object: f.object %>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.email_field :email, class: 'form-control' %>
        </div>
        <div class="form-group">
          <%= f.label :last_name %>
          <%= f.text_field :last_name, class: 'form-control' %>
        </div>
        <div class="form-group">
          <%= f.label :first_name %>
          <%= f.text_field :first_name, class: 'form-control' %>
        </div>
        <div class="form-group">
          <%= f.label :avatar %>
          <%= f.file_field :avatar, onchange: 'previewImage(preview)', class: 'form-control', accept: 'image/*' %>
          <%= f.hidden_field :avatar_cache %>
        </div>
        <div class='mt-3 mb-3'>
          <%= image_tag @user.avatar.url,
                        class: 'rounded-circle',
                        id: 'preview',
                        size: '100x100' %>
        </div>
        <div class="form-group">
          <%= f.label :role %>
          <%= f.select :role, User.roles_i18n.invert, {}, class: 'form-control' %>
        </div>
        <%= f.submit class: 'btn btn-primary' %>
      <% end %>
    </div>
  </div>
</div>

掲示板一覧画面

<% content_for(:title, t('.title')) %>
<div class="container mb-5 pt-2">
  <h1><%= t('.title') %></h1>
  <div class="row">
    <div class="col-md-12 mb-3">
      <%= render 'search_form' %>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <table class="table table-striped">
        <thead>
        <tr>
          <th scope="col"><%= Board.human_attribute_name(:id) %></th>
          <th scope="col"><%= Board.human_attribute_name(:title) %></th>
          <th scope="col"><%= Board.human_attribute_name(:user) %></th>
          <th scope="col"><%= Board.human_attribute_name(:created_at) %></th>
          <th scope="col"></th>
        </tr>
        </thead>
        <tbody>
        <%= render @boards %>
        </tbody>
      </table>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <!-- ページネーション -->
      <%= paginate @boards %>
    </div>
  </div>
</div>

一覧画面に個々の掲示板を表示するパーシャルを作成

<tr>
  <td>
    <%= board.id %>
  </td>
  <td>
    <%= board.title %>
  </td>
  <td>
    <%= board.user.decorate.full_name %>
  </td>
  <td>
    <%= l board.created_at, format: :long %>
  </td>
  <td>
    <%= link_to t('defaults.show'), admin_board_path(board), class: 'btn btn-info' %>
    <%= link_to t('defaults.edit'), edit_admin_board_path(board), class: 'btn btn-success' %>
    <%= link_to t('defaults.delete'), admin_board_path(board), method: :delete, data: { confirm: t('defaults.message.delete_confirm') }, class: 'btn btn-danger' %>
  </td>
</tr>

検索フォームのパーシャルを作成する

掲示板検索フォームもパーシャル化して用意します。

ここではユーザー検索と違って、「タイトルor本文に含まれるかどうか判定」と「作成日の日時範囲指定」を行います。

<%= search_form_for @search, url: admin_boards_path do |f| %>
  <div class="row">
    <div class="form-inline align-items-center mx-auto">
      <div class="col-auto">
        <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: t('defaults.search_word') %>
      </div>
      <div class="col-auto">
        <%= f.date_field :created_at_gteq, class: 'form-control' %>
        <span>~</span>
        <%= f.date_field :created_at_lteq_end_of_day, class: 'form-control' %>
      </div>
      <div class="col-auto">
        <%= f.submit class: 'btn btn-primary' %>
      </div>
    </div>
  </div>
<% end %>

やや複雑な説明になりますので、別の記事で解説します。

掲示板詳細画面

<% content_for(:title, @board.title) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1><%= t('.title') %></h1>
      <div class="text-right mb-3">
        <%= link_to t('defaults.edit'), edit_admin_board_path(@board), class: 'btn btn-success' %>
        <%= link_to t('defaults.delete'), admin_board_path(@board), method: :delete, data: { confirm: t('defaults.message.delete_confirm') }, class: 'btn btn-danger' %>
      </div>
      <table class="table table-bordered bg-white">
        <tr>
          <th scope="row"><%= Board.human_attribute_name(:id) %></th>
          <td><%= @board.id %></td>
        </tr>
        <tr>
          <th scope="row"><%= Board.human_attribute_name(:title) %></th>
          <td><%= @board.title %></td>
        </tr>
        <tr>
          <th scope="row"><%= Board.human_attribute_name(:user) %></th>
          <td><%= @board.user.decorate.full_name %></td>
        </tr>
        <tr>
          <th scope="row"><%= Board.human_attribute_name(:body) %></th>
          <td><%= @board.body %></td>
        </tr>
        <tr>
          <th scope="row"><%= Board.human_attribute_name(:created_at) %></th>
          <td><%= l @board.created_at, format: :long %></td>
        </tr>
      </table>
    </div>
  </div>
</div>

掲示板編集画面

<% content_for(:title, @board.title) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1><%= t '.title' %></h1>
      <%= form_with model: @board, url: admin_board_path(@board), local: true do |f| %>
        <%= render 'shared/error_messages', object: f.object %>
        <div class="form-group">
          <%= f.label :title %>
          <%= f.text_field :title, class: 'form-control' %>
        </div>
        <div class="form-group">
          <%= f.label :body %>
          <%= f.text_area :body, class: 'form-control', rows: 10 %>
        </div>
        <div class="form-group">
          <%= f.label :board_image %>
          <%= f.file_field :board_image, class: 'form-control mb-3', accept: 'image/*', onchange: 'previewImage(preview)' %>
          <%= f.hidden_field :board_image_cache %>
        </div>
        <div class='mt-3 mb-3'>
          <%= image_tag @board.board_image.url, id: 'preview', size: '300x200' %>
        </div>
        <%= f.submit class: 'btn btn-primary' %>
      <% end %>
    </div>
  </div>
</div>

ロケールファイルの設定

defaults: #すべてのコントローラーで使える
    login: 'ログイン'
    register: '登録'
    logout: 'ログアウト'
    post: '投稿'
    search_word: '検索ワード'
    show: '詳細'
    edit: '編集'
    delete: '削除'
    password_reset: 'パスワードリセット'
    unspecified: '指定なし'

admin:
    user_sessions:
      new:
        title: 'ログイン'
      create:
        success: 'ログインしました'
        fall: 'ログインに失敗しました'
      destroy:
        success: 'ログアウトしました'
    dashboards:
      index:
        title: 'ダッシュボード'
    users:
      index:
        title: 'ユーザー一覧'
      show:
        title: 'ユーザー詳細'
      edit:
        title: 'ユーザー編集'
    boards:
      index:
        title: '掲示板一覧'
      show:
        title: '掲示板詳細'
      edit:
        title: '掲示板編集'
        board_image: 'サムネイル'
        user: '作成者'
      comment:
        body: 'コメント'
  attributes:
    id: 'ID'
    created_at: '作成日時'
    updated_at: '更新日時'
  enums:
    user:
      role:
        general: '一般'
        admin: '管理者'

終わりに

中には難しい実装も有りました。お疲れさまでした。

参考記事

form_forの使い方をマスターしよう!
railsのform_forの使い方をどこよりもわかりやすく解説しています。使用できるhtmlタグが実際にどのように表示されるか、具体的にどう書けばいいのかをこの記事を読めばform_forを思い通りに使いこなすことができます。
https://github.com/activerecord-hackery/ransack/blob/master/lib/ransack/locale/en.yml#L15

コメント

タイトルとURLをコピーしました