はじめに
管理画面に、掲示板とユーザの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
コメント