Model Spec

概要

taskモデルのバリデーションに関するテストを作成する。
FactoryBotを使用しテストデータを作成する。

バリデーションエラーの発生と、エラーメッセージの内容を表示させる。
各バリデーションを削除した際、テストが失敗することを確認する。

FactoryBotの設定

FactoryBot.の省略
以下の設定を追加すると、factory_botメソッドの前にFactoryBot.を付ける必要がなくなる。
FactoryBot.create(:user)create(:user)と省略して書ける。

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

UserとTaskモデルのテストデータを作成

  FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    content { 'Content' }
    status { 'todo' }
    deadline { Date.current.tomorrow }
    association :user
  end
end
  FactoryBot.define do
    factory :user do
      sequence(:email) { |n| "tester#{n}@example.com" }
      password { 'password' }
      password_confirmation { 'password' }
    end
  end

今回のUserとTaskのアソシエーションは1対多(belongs_to と has_many)である。
ユニーク制約のついているカラムではsequenceメソッドを使用する。
association :userでuserとの関連を定義。

sequenceメソッド

sequenceの第二引数で定義した文字にはループの度に.nextが実行される。

> "title_1".next
 => "title_2"

sequence(:title){|n| "title_#{n}"}sequence(:title, "title_1")のように書ける。

TaskのModel Spec

taskモデルのvalidationに関するテストを記述していく。

  • 全部の属性を登録したとき有効
  • titleがないときに無効
  • titleが重複したとき無効
require 'rails_helper'

RSpec.describe Task, type: :model do
  describe 'validation' do
    it "is valid with all attributes" do
      task = build(:task)
      expect(task).to be_valid
      expect(task.errors).to be_empty
    end

    it "is invalid without a title" do
      task_without_title = build(:task, title: "")
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to eq ["can't be blank"]
    end

    it "is invalid with a duplicate title" do
      task = create(:task)
      task_with_duplicated_title = build(:task, title: task.title)
      expect(task_with_duplicated_title).to be_invalid
      expect(task_with_duplicated_title.errors[:title]).to eq ["has already been taken"]
    end
  end
end

describe' 'do...endでテスト内容の宣言。
今回はvalidationに関するテストを宣言している。

it " " do...endには、実行するテスト内容を記述する。

be_○○○マッチャ

Rspecでは、empty? のようにメソッド名が?で終わり、戻り値が true / false になるメソッドを be_empty のような形式で検証できる。
expect(task).to be_validで記述するのがexpect(期待する)で、ここでは
taskがto be_validすることを期待するというテストとなる。
be_validはマッチャと呼ばれる機能でexpectの引数にしたインスタンスにバリデーションエラーが発生しないことを検証する。(task.valid? が true になればパスする)

expect(task.errors).to be_emptyでtask.errorsがemptyであることを期待する。

省略

task = FactoryBot.build(:task, title: "", user: user)task = build(:task)と書ける。
FactoryBot.buildでTaskのFactoryにuserオブジェクトを指定。
設定ファイルへ追記しているので、FactoryBot.の部分は省略できる。
taskのFactoryにassociationを定義しているのでuserの記述を省略できる。

buildとcreateの違い

buildはテストデータをメモリ上に保存するが、createはデータベース上に保存する。
データベースへのアクセスが必要なテストの場合はcreateを使用し、必要でない場合はbuildを使えば良い
以下、idや作成日時を見るとbuildではnilとなっていて、createではデータベース上に保存されているので値が表示されている。
f:id:meo2:20210731174744p:plain

rspecセットアップ

概要

GitHubからサンプルアプリを fork, clone 後、ローカルでのrspec実行環境をセットアップする。

ローカルへのコピー

GitHubからサンプルアプリをforkし、ターミナルからcloneする。
$ git clone URL
新たなブランチの作成
$ git checkout -b ブランチ名

セットアップ

gem rspecを参考にインストール。
gem 'factory_bot_rails'
gem 'rspec-rails', '~> 5.0.0'の導入。
group :development, :test へ追記しbundleする。
(開発環境でもrails console等で動作確認ができる。)

group :development, :test do
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
  gem 'factory_bot_rails'
  gem 'rspec-rails', '~> 5.0.0'
end  

ジェネレーターを使ってrspecに必要な設定ファイルをインストールする。

$ rails generate rspec:install
      create .rspec
      create spec
      create spec / spec_helper.rb
      create spec / rails_helper.rb

ジェネレーターを使ってtaskモデルのspecの雛形を作成。

$ rails generate rspec:model task
      create spec/models/task_spec.rb
      invoke factory_bot
      create spec / factories/tasks.rb

rspecコマンドでテストが実行できるか確認。

$ bundle exec rspec            
*

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Task add some examples to (or delete) /Users/owner/rspec1/sample_app_for_rspec/spec/models/task_spec.rb
     # Not yet implemented
     # ./spec/models/task_spec.rb:4


Finished in 0.00168 seconds (files took 0.61562 seconds to load)
1 example, 0 failures, 1 pending

Git,GitHub基礎

Gitとは

ファイルのバージョン管理をするツールのこと。
バージョン管理とは、ソースコードなどのファイルの追加や変更履歴を管理することで、過去の変更を確認したり、特定の場所にファイルを戻したりできること。

バージョン管理を使えば、いつ、どこで、誰が、どのようなファイル変更をしたのか記録でき、いつでも以前の状態に戻せる。

バージョン管理には分散型と集中型の2種類があり、Gitは分散型のバージョン管理システムである。
分散型バージョン管理システムであるGitは開発者自身のPCにローカルリポジトリを持ち、ここに修正や追加したソースコードをcommitすることができる。

ローカルリポジトリへcommitされたソースコードはリモートリポジトリへpushすることでローカルとリモートのリポジトリ内容を合わせることができる。

GitHubとは

ソースコードの管理サービスのことで、主にリモートリポジトリとして活用される。
Gitで管理しているソースコードをチームで簡単に共有できるwebサービス

リポジトリ

バージョン管理によって管理されたファイルやディレクトリの状態や変更履歴を保管するところをリポジトリと呼ぶ。

リポジトリの種類

f:id:meo2:20210723203727p:plain

開発過程をリモートリポジトリへ登録する流れ

  • ステージング(add): ワークツリー(実際に作業してるディレクトリ)で変更があるファイルをステージングエリアへ追加する
  • コミット(commit): ステージングエリアからローカルリポジトリへ登録
  • プッシュ(push): ローカルリポジトリに登録されたファイルをリモートリポジトリへ追加

    コマンド

    $ git status(ステージングにファイルが存在しているか確認)
    $ git add -A( 変更があったすべてのファイルがaddされる) この状態をステージングという。
    $ git commit -m '(適切な内容のコミットメッセージ)'
    $ git push origin(リモートへ変更を反映させる)
    $ git rm -r cached ファイル名(リモートからファイルを削除する。)
    cachedオプションをつけないとローカルからも削除される。
    .gitignoreへ削除したいディレクトリを指定して実行。
    .gitignoreとは、Gitの管理対象に含めないファイルを指定するファイル。

    pullとfetch

  • pull: リモートリポジトリの変更履歴をローカルへ取得。
    リモートのmasterブランチからorigin/masterを介してローカルのmasterまで一気に変更する。
    $ git checkout master
    masterブランチへ移動
    $ git pull origin master
    origin(複製元リモートリポジトリ)の変更をローカルへ取り込む

  • fetch: リモートリポジトリの変更履歴をローカルへ取得。
    $ git fetch origin master
    pullとは違い、origin/masterに変更が取り込まれる。
    $ git merge origin master
    マージしてorigin/masterからmasterへ変更を取り込む。

リポジトリのcloneとfork

既にリモートリポジトリが存在する場合、リモートリポジトリをローカルリポジトリへcloneする。
cloneとは、リモートリポジトリからローカルリポジトリへ複製すること。
cloneすると複製元のリモートリポジトリにはoriginという名前がつく。
$ git clone URL
URLはGitHubから確認する。
f:id:meo2:20210724043336p:plain:w300:h220
forkとは、他人のリモートリポジトリを自分のアカウントのリモートリポジトリへ複製すること。
forkするにはGitHubで右上にあるforkボタンを押すだけ
f:id:meo2:20210724044845p:plain:w300:h50
forkしたら複製した自分のリモートリポジトリをcloneして開発を進めていく。

ブランチ

Gitにはブランチ(branch)という機能があり、
masterから変更履歴を分岐させ、新しい作業場所を作ることをブランチを切ると言う。
f:id:meo2:20210724162956p:plain
ブランチの作成
$ git checkout -b ブランチ名
現在のブランチの確認
$ git branch

GitHub Flow

GitHubではmasterからブランチを切って開発を進め、リモートにpushした後、プルリクエストと呼ばれる機能でコードレビューを行いマージをする。(この開発の流れをGitHub Flowという)
f:id:meo2:20210724195935p:plain

実践

GitHubリポジトリを作成し、cloneする。
$ git clone URL
f:id:meo2:20210724201303p:plain
ローカルでプルリクエスト用のブランチを切る。
$ git checkout -b pullreq1
リモートへpushする。
$ git add -A
$ git commit -m ''
$ git push
GitHub上でプルリクエストを作成する。
GitHub上のリポジトリページからPull requestsを選択し、
マージ先のブランチとプルリクエスト対象のブランチを選択する。
f:id:meo2:20210725052931p:plain

コミット履歴やファイル差分を確認して、Create pull requestする。

f:id:meo2:20210725053231p:plain

プルリクエストを受け取ったユーザーはFiles changedからソースコードを確認し、修正がある場合は commentから修正を促す。変更に問題がなければ、Merge pull requestを実行する。
pullreq1ブランチの内容がmasterへ反映される。

f:id:meo2:20210725053920p:plain

マージ後、pullreqリポジトリのmasterブランチを表示すると、プルリクエストでpullreq1ブランチをmasterブランチへ取り組んだことによりREADME.mdの記述が変わっていることが確認できる。

f:id:meo2:20210725055521p:plain

javascriptDOMの操作

DOMとは

DOM( Document Object Model )はHTMLやXMLを取り扱うためのAPIのこと。 DOMを使えば、HTMLの探索やスタイルの変更・イベントの設定・HTML要素の取得の他に、振る舞いを変えたり、ユーザー操作時の処理を設定することができる。

htmlを用意する

DOMを操作するにはidやclassが必要
テキストエリアとボタンを作成

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>DOM1</title>
  <style>
  </style>
</head>
<body>
    <textarea id="textarea"></textarea>
    <button id="button">ボタン</button>
</body>
</html>

以下のように表示される
f:id:meo2:20210721163113p:plain:w200:h50

練習: ボタンをクリックしたらテキストの入力値を取得しアラートとして表示させるようにする

scriptタグを追加し、要素を記述していく。

~
<body>
    <textarea id="textarea"></textarea>
    <button id="button">ボタン</button>
    
    <script>
        'use strict';
    const button = document.getElementById('button')
    button.addEventListener('click', () => {
      const textarea = document.getElementById('textarea')
      alert(textarea.value)
    })
    </script>
</body>
~

documentオブジェクト

documentには、HTML、CSSJavaScriptなど全体のドキュメントが入っている。
documentオブジェクトで要素を取得し、変数buttonへ代入する。
getElementById('button')でbuttonというidの要素を取得する。idはDOMツリー中で一意である必要がある。
コンソールに入力すると、#documentという、HTMLファイル内の情報全てを取得することができる。 f:id:meo2:20210721173409p:plain

イベントトリブンの設定

イベントドリブンとは、なんらかのアクション(ボタンをクリックする、画面のリロード、など)をしたら何かが起きる処理のこと。
先程定義したDOMに対して、addEventListenerを設定してあげることでイベントを仕掛けることができる。
button.addEventListener('click', () => { })でbuttonをクリックしたときのイベントを設定できる。

テキストの入力値を取得してアラート表示させたいので、テキストのDOMを取得する。
const textarea = document.getElementById('textarea')
alert(textarea.value)valueで入力値を取得。
これで、アラートが表示されるようになる。
f:id:meo2:20210721172936p:plain

gem enum_helpを使用し、enum値のi18n化と権限のプルダウンを実装

前提

enumとransackは導入済み
enum
ransack

実装内容

enumで定義した値の翻訳
ユーザー一覧画面でのプルダウン検索
f:id:meo2:20210712031757p:plain:w500:h200

導入

gem 'enum_help'

bundle install

enumの設定

  • general・・・一般
  • admin・・・管理者 と定義する。
  enum role: { general: 0, admin: 1 }

i18nの設定

 enums:
   user:
     role:
       general: '一般'
       admin: '管理者'  

view

viewで以下のように記述することで呼び出せる。

<%= user.role_i18n %>

プルダウンの実装

controller

  def index
    @q = User.ransack(params[:q])
    @users = @q.result(distinct: true).page(params[:page])
  end

params[:q]で検索フォームに入力した文字を取得(viewから送られてきたパラメータ)デフォルトで:qとなっている。→インスタンス変数@qを定義。
ransackメソッド、送られてきたパラメータをもとにテーブルからデータを検索。
resultメソッドでransackメソッドで取得したデータをオブジェクトに変換し、検索結果を渡す。
distinct: trueで重複を防ぐ
pageメソッドでページネーションの実装。

view

  <%= search_form_for @q, url: admin_users_path do |f| %>
    <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, placeholder:'検索ワード', class:'form-control' %>
            </div>
            <div class="col-auto">
              <%= f.select :role_eq, User.roles_i18n.invert.map { |key, value| [key, User.roles[value]]}, include_blank:'指定なし', class:'form-control mr-1' %>
            </div>
            <div class="col-auto">
              <%= f.submit '検索', class:'btn btn-primary' %>
            </div>
        </div>
    </div>
<% end %>

search_form_forメソッドはransackで用意されているメソッド
form_withやform_forと同じ
検索オブジェクトqを指定。
urlオプションでリクエストするurlを渡す。→renderでパーシャルを読み込む時に指定。

<%= f.select :role_eq, 
User.roles_i18n.invert.map { |key, value| [key, User.roles[value]]}, 
include_blank:'指定なし', #第3引数 
class:'form-control mr-1' %> #第4引数,htmlオプション

について
role_eq、eq(equals)は ransackのメソッド、等しい値を持つレコードを返す。
invertメソッドでkeyとvalueを入れ替える
mapメソッド配列.map {|変数名| 具体的な処理 }で戻り値を配列へ返す。
include_blankオプションは先頭に表示されるメッセージに空白行を表示する。

f.select

f.selectを使うと、セレクトボックスの選択項目や選択された時の値や、セレクトボックスに設定したいオプションを指定できる。

<%= f.select :保存されるカラム名, [ ["表示される文字","保存される値"], {オプション}, {htmlオプション} ] %>

User.rolesでuserモデルのroleの値(ハッシュ)を取得
{キー1 => 値1, キー2 => 値…}

pry(main)> User.roles
=> {"general"=>0, "admin"=>1}

enum_helpの導入により、User.roles_i18nというメソッドを使えるようになる。
i18nで設定した日本語を適用

pry(main)> User.roles_i18n
=> {"general"=>"一般", "admin"=>"管理者"}

invertメソッドでハッシュのkeyとvalueを入れ替える。
f.select :保存されるカラム名, [ ["表示される文字","保存される値"]に合わせたいので、ハッシュを入れ替える。
こうすることで、プルダウンが英語→日本語に切り替わる。

pry(main)> User.roles_i18n.invert
=> {"一般"=>"general", "管理者"=>"admin"}

mapメソッドでハッシュを配列にする。
一般、 管理者というキーをkey に、general 、adminという値が valueへ代入。

pry(main)> User.roles_i18n.invert.map { |key, value| [key, User.roles[value]]}
=> [["一般", 0], ["管理者", 1]]

ransackはenumで定義したstringに対応していないので、integerにする必要がある。
[key, User.roles[value]]valueをintegerにしている。

pry(main)> User.roles['general']
=> 0
pry(main)> User.roles['admin']
=> 1

メニューのアクティブ・非アクティブ化

管理画面において、以下のようにサイドバーで選択(クリック)した部分を青色(アクティブ)にさせる。
f:id:meo2:20210712020932p:plain:w200:h180

実装

ヘルパーにメソッドを定義する。

  def active_if(path)
    path == controller_path ? 'active' : ''
  end

controller_path?でcontroler名を取得する。
controller_path?がtrueの場合、activeを返し、そうでない場合何も返さない。
三項演算子
if文の単純な分岐を短く書くことができる。

if 条件
 式1
else
 式2
end
条件 ? 式1 : 式2

上記で定義したヘルパーメソッドをview側へ記述する。

  <%= link_to admin_boards_path, class: "nav-link #{active_if('admin/boards')}" do %>
  <%= link_to admin_users_path, class: "nav-link #{active_if('admin/users')}" do %>

path == controller_path ?により、リンク先に指定したpathとコントローラーが等しければactiveを返す。(青色になる)

管理画面のCRUD作成

CRUD作成

以下のような、管理画面に掲示板とユーザーのCRUD(Create(生成)、Read(読み取り)、Update(更新)、Delete(削除))機能を作成する。 f:id:meo2:20210710180941p:plain

ルーティング

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

f:id:meo2:20210710180722p:plain

掲示板のCRUD作成

掲示板一覧では、ID、タイトル、作成者、作成日の項目を表示させる。
それぞれに対して「詳細」「編集」「削除」ボタンの項目を表示させ、削除時には確認アラートを表示させる。

controller

boards_controllerの作成
$rails g controller admin::boards
継承元をbase_controlerにする。
ransackで検索機能をつける。→参考

  class Admin::BoardsController < Admin::BaseController
  before_action :set_board, only: %i[edit show update destroy]
  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
  end

  def edit; end

  def show
    @board = Board.find(params[:id])
    @comment = Comment.new
    @comments = @board.comments.includes(:user).order(created_at: :desc)
  end

  def update
    if @board.update(board_update_params)
      redirect_to admin_board_path(@board), success: '掲示板を更新しました'
    else
      flash.now['danger'] = '編集に失敗しました'
    end
  end

  def destroy
    @board.destroy!
    redirect_to admin_boards_path, success: '掲示板を削除しました'
  end

  private

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

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

掲示板一覧のviewを作成

パーシャルを使い一覧画面を表示させる。

  <%= content_for(:title, t('.title')) %>
 <div class="container mb-5 pt-2">
   <h1>掲示板一覧</h1>
   <div class="row">
     <div class="col-md-12 mb-3">
       <%= render 'search_form' %>
     </div>
   </div>
   <div class="row">
     <div class="col-sm-12">
       <table class="table table-striped">
         <thead>
         <tr>
           <th>ID</th>
           <th>タイトル</th>
           <th>作成者</th>
           <th>作成日時</th>
           <th></th>
         </tr>
         </thead>
         <tbody>
         <%= render @boards %>
         </tbody>
       </table>
     </div>
   </div>
   <div class="row">
     <div class="col-sm-12">
       <!-- ページネーション -->
       <%= paginate @boards %>
     </div>
   </div>
 </div>

検索機能のパーシャル

  <%= search_form_for @q, url: 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, placeholder:'検索ワード', class:'form-control' %>
      </div>
      <div class="col-auto">
        <%= f.date_field :created_at_gteq, include_blank: true, class:'form-control' %><%= f.date_field :created_at_lteq_end_of_day, include_blank: true, class:'form-control' %>
      </div>
      <div class="col-auto">
        <%= f.submit '検索', class:'btn btn-primary' %>
      </div>
    </div>
  </div>
<% end %>

リンクなどのパーシャル

  <tr>
    <td>
        <%= board.id %>
    </td>
    <td>
        <%= link_to board.title, admin_board_path(board) %>
    </td>
    <td>
        <%= board.user.decorate.full_name %>
    </td>
    <td>
        <%= l board.created_at, format: :long %>
    </td>
    <td>
        <%= link_to '詳細', admin_board_path(board), class:"btn btn-info" %>
        <%= link_to '編集', edit_admin_board_path(board), class:"btn btn-success" %>
        <%= link_to '削除', admin_board_path(board), method: :delete, data: { confirm: '本当に削除しますか?' }, class:"btn btn-danger" %>
    </td>
</tr>

編集画面のview

form_withの書き方
<%= form_with model: @board, url: admin_boards_path, local: true do |f| %>とURLを指定しなくても、簡潔に書ける。
<%= form_with model: [:admin, @board], local: true do |f| %>
form_withの引数に[:admin, @board]を渡すことで、
/admin/boardsというurlをRailsが生成してくれる。

  <% 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: [:admin, @board], local: true do |f| %>

        <%= render 'shared/error_messages', object: f.object %>
        <div class="form-group">
          <%= f.label :title, Board.human_attribute_name(:title) %>
          <%= f.text_field :title, class: 'form-control' %>
        </div>

        <div class="form-group">
          <%= f.label :body, Board.human_attribute_name(:body) %>
          <%= f.text_area :body, class: 'form-control' %>
        </div>

        <div class="form-group">
          <%= f.label :board_image %>
          <%= f.file_field :board_image, onchange: 'previewImage()', class: 'form-control mb-3', accept: 'image/*' %>
          <%= 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>

f:id:meo2:20210712004504p:plain:w400:h250

詳細画面のview

<%= @board.user.decorate.full_name %>
user_decorator.rbfull_nameメソッドを呼び出し、ユーザー名を表示する。
<%= l @board.created_at, format: :long %>
lメソッドはi18nにおいて日付や時刻を表す際に使用する。
config/locales/ja.ymlに書かれた内容を読み込む。
formatオプションを使うことで複数の書式を使い分けることができる。

  <% 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 '編集', edit_admin_board_path(@board), class:"btn btn-success" %>
        <%= link_to '削除', admin_board_path(@board), method: :delete, data: { confirm: '本当に削除しますか?' }, class:"btn btn-danger" %>
      </div>
      <table class="table table-bordered bg-white">
        <tbody><tr>
          <th scope="row">ID</th>
          <td><%= @board.id %></td>
        </tr>
        <tr>
          <th scope="row">タイトル</th>
          <td><%= @board.title %></td>
        </tr>
        <tr>
          <th scope="row">作成者</th>
          <td><%= @board.user.decorate.full_name %></td>
        </tr>
        <tr>
          <th scope="row">本文</th>
          <td><%= @board.body %></td>
        </tr>
        <tr>
          <th scope="row">作成日時</th>
          <td><%= l @board.created_at, format: :long %></td>
        </tr>
      </tbody></table>
    </div>
  </div>
</div>

f:id:meo2:20210712004521p:plain:w500:h250

ユーザー一覧画面のviewを作成

一覧画面と同じくパーシャルを使用する。

  <% content_for(:title, t('.title')) %>
 <div class="container mb-5 pt-2">
   <h1>ユーザー一覧</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>ID</th>
           <th>氏名</th>
           <th>権限</th>
           <th>日時</th>
           <th></th>
         </tr>
         </thead>
         <tbody>
         <%= render @users %>
         </tbody>
       </table>
     </div>
   </div>
   <div class="row">
     <div class="col-md-12">
       <!-- ページネーション -->
       <%= paginate @users %>
     </div>
   </div>
</div>

検索機能のパーシャル

  <%= search_form_for @q, url: admin_users_path do |f| %>
    <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, placeholder:'検索ワード', class:'form-control' %>
            </div>
            <div class="col-auto">
              <%= f.select :role_eq, User.roles_i18n.invert.map { |key, value| [key, User.roles[value]]}, include_blank:'指定なし', class:'form-control mr-1' %>
            </div>
            <div class="col-auto">
              <%= f.submit '検索', class:'btn btn-primary' %>
            </div>
        </div>
    </div>
<% end %>

リンクなどのパーシャル

  <tr>
    <td>
        <%= user.id %>
    </td>
    <td>
        <%= link_to user.decorate.full_name, admin_user_path(user) %>
    </td>
    <td>
        <%= user.role_i18n %>
    </td>
    <td>
        <%= l user.created_at, format: :long %>
    </td>
    <td>
        <%= link_to '詳細', admin_user_path(user), class:"btn btn-info" %>
        <%= link_to '編集', edit_admin_user_path(user), class:"btn btn-success" %>
        <%= link_to '削除', admin_user_path(user), method: :delete, data: { confirm: '本当に削除しますか?' }, class:"btn btn-danger" %>
    </td>
</tr>

編集画面のview

  <% 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: [:admin, @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()', class: 'form-control mb-3', 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>

f:id:meo2:20210712010953p:plain:w400:h400

詳細画面のview

  <% 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 '編集', edit_admin_user_path(@user), class:"btn btn-success" %>
        <%= link_to '削除', admin_user_path(@user), method: :delete, data: { confirm: '本当に削除しますか?' }, class:"btn btn-danger" %>
      </div>
      <table class="table table-bordered bg-white">
        <tbody>
        <tr>
          <th scope="row">ID</th>
          <td><%= @user.id %></td>
        </tr>
        <tr>
          <th scope="row">権限</th>
          <td>
            <%= @user.role_i18n %>
          </td>
        </tr>
        <tr>
          <th scope="row">氏名</th>
          <td><%= @user.decorate.full_name %></td>
        </tr>
        <tr>
          <th scope="row">アバター</th>
          <td><img src="/assets/sample-08ca6735af47bfefce4c43e3de2182768289121aa057f141b7f4704a89670827.jpg"></td>
        </tr>
        <tr>
          <th scope="row">作成日時</th>
          <td><%= l @user.created_at, format: :long %></td>
        </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

f:id:meo2:20210712011030p:plain:w400:h400

i18nの設定

  admin:
     users:
      index:
       title: 'ユーザー一覧'
      new:
       title: 'ユーザー登録'
      show:
       title: 'ユーザー詳細'
      edit:
       title: 'ユーザー編集'
     boards:
      index:
       title: '掲示板一覧'
      show:
       title: '掲示板詳細'
      edit:
       title: '掲示板編集'
     dashboards:
       index:
         title: 'ダッシュボード'