Railsで管理画面 - 掲示板 編

前回の続き

michimo-10.hatenablog.com

 

・コントローラー


adminの時はbase_controllerを使っているので、今まで使っていたApplicationControllerで作ったboards_controllerやuser_controller。。。などは引き継がれない(引っ張ってくることが出来ない)

つまり、もう一度boards_controllerやuser_controllerを定義し直さなければならない

とは言え一度作成しているので使いたい中身はほぼ同じ。ただ挙動を変える必要があるくらいだ。

また、今課題には今までに無かったuserの一覧画面の作成があるのでuserの情報を取って来なければならない。その辺りはコントローラーで明記し、ビューで表示させる。


rails g controller admin::Boards # この時オプションでビューも作っていいがビューは後から個別で作る

今まで作ってきた元々のboardsコントローラーの中身を上で作ったコントローラー内に移植し部分修正

・自分の回答 # controllers/admin/boards_controller

class Admin::BoardsController < Admin::BaseController
  before_action :set_params, only: %i[edit show update destroy]
  def index # ↓回答例と違う変数なのでindexのビューで呼ぶ時は@searchを使う
    @search = Board.ransack(params[:q])  #bookmarkは要らない↓と思ったのでuserだけ
    @boards = @search.result(distinct: true).includes(%i[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, 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_params
    @board = Board.find(params[:id]) # ここで詰まってた
  end

  def board_params
    params.require(:board).permit(:title, :body, :board_image, :board_image_cache)
  end
end
  • ・回答例 # controllers/admin/boards_controller.rb

    class Admin::BoardsController < Admin::BaseController
       before_action :set_board, only: %i[edit update show 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 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 show; 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
    

・ビュー


コントローラーでboardのidを取ってくることが出来たので、

・一覧(index),詳細(show),編集(edit)ページを作成する。 ・ファイルとフォルダは新たに作成しadmin下にすること。

一覧表示と言っても今までのページの表示ではなく管理者用のページなので画像は必要なく、リスト化された物で課題詳細にあったように

掲示板一覧では、ID、タイトル、作成者、作成日の項目を表示

が出来れば良い。

参考にするのは以下のBootstrap

Tables

Buttons

一覧画面(index)


・自分の回答 # views/admin/boards/index.html.erb

<%= content_for(:title, t('.title')) %>  # ページタイトル部分
  <h1><%= t('.title') %></h1> # タイトル部分は元々のビューから引っ張ってきた
<div class="col-lg-10 offset-lg-1">
  <!-- 検索フォーム -->
  <form>
  <%= render 'search_form', url: admin_boards_path, search: @search %>
  </form> # ↑ 検索フォームもパーシャルにした、パスも気をつける事 - 後述
</div>
<table class="table table-striped">
  <thead>
    <tr>
			<th scope="col">id</th>
      <th scope="col">タイトル</th>
      <th scope="col">作成者</th>
      <th scope="col">作成日時</th>
    </tr>
  </thead>
  <tbody>
    <%= render @boards %> # パーシャル作った
  </tbody>
</table>
  • ・回答例 # views/admin/boards/index.html.erb

    <% 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-sm-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-sm-12">
           <!-- ページネーション -->
           <%= paginate @boards %>
         </div>
       </div>
     </div>
    

回答例にはページネーションが付いてた。 また、<th>タグの中の文字もきちんとja.ymlに記載しているが故の書き方になっている(activerecordの方) 検索のパーシャル部分もスマートになっている

パーシャル部分


・自分の回答 # views/admin/boards/_board.html.erb

<tr>
  <th scope="row"><%= board.id %></th>
  <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 '編集', edit_admin_board_path(board), class:"btn btn-success", id: "button-edit-#{board.id}" %>
  </td>
  <td><%= link_to '削除', admin_board_path(board), id: "button-delete-#{board.id}", class:"btn btn-danger",
        method: :delete, data: {confirm: t('defaults.message.d_confirm')} %>
      </td>
</tr>
  • ・回答例 # views/admin/boards/_board.html.erb

    <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 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>
    

検索のパーシャル


・自分の書き方 # /views/admin/boards/_search_form.html.erb

<div class="input-group mb-3">
  <%= search_form_for search, url: url do |f| %>
    <div class="form-inline align-items-center mx-auto">
      <div class="col-auto">
        <%= f.search_field :title_or_body_cont, class:"form-control", placeholder:"検索ワード" %>
      </div>
      <div class="col-auto">
      <%= f.date_field :created_at_gteq, include_blank: true, class: 'form-conrol' %>
        <span>~</span>
      <%= f.date_field :created_at_lteq_end_of_day, include_blank: true, class: 'form-conrol' %>
      </div>
      <div class="col-auto">
      <%= f.submit class:"btn btn-primary input-group-append" %>
      </div>
    </div>
  <% end %>
</div>
  • ・回答例 # /views/admin/boards/_search_form.html.erb

    <%= search_form_for @q, 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 %>
    

後述するが :created_at_gteq と言う部分が、今回の検索機能の「〜から〜まで」の部分のメソッドになる

form_forで使用できるhtmlタグ

f.selectやf.date_selectを使うことで選択肢や日付選択ボックスを生成してくれる。 よく見かけるf.submitが送信ボタンの生成してくれる様に。

form_forの使い方をマスターしよう!

セレクトボックスの書き方は以下

【開発メモ】Ruby on Railsのform_forでドロップダウンリストの選択ボックスを設置する方法 | FREE SWORDER

詳細(show)


・自分の回答 # views/admin/boards/show.html.erb

<% content_for(:title, @board.title) %>
 <div class="container pt-5">
   <div class="row mb-3">
     <div class="col-lg-8 offset-lg-2">
       <h1><%= t('.title') %></h1>
       <!-- 掲示板内容 -->
       <article class="card">
         <div class="card-body">
           <div class='row'>
             <div class='col-md-3'>
               <%= image_tag @board.board_image.url, class: 'card-img-top img-fluid', size: '300x200' %>
             </div>
             <div class='col-md-9'>
               <h3 class="d-inline"><%= @board.title %></h3>
               <%= render 'crud_menus', board: @board %>  # 元々の掲示板同様にアイコンの編集削除のパーシャルを作った
               <ul class="list-inline">
                 <li class="list-inline-item">by <%= @board.user.decorate.full_name %></li>
                 <li class="list-inline-item"><%= l @board.created_at, format: :long %></li>
               </ul>
             </div>
           </div>
           <p><%= simple_format(@board.body) %></p>
         </div>
       </article>
     </div>
   </div>
</div>

※自作のパーシャル部分 # admin/boards/_crud_menus.html.erb

<ul class='crud-menu-btn list-inline float-right'>
  <li class="list-inline-item">
    <%= link_to t('defaults.edit'), edit_admin_board_path(board), id: "button-edit-#{board.id}" %>
  </li>
  <li class="list-inline-item">
    <%= link_to t('defaults.delete'), admin_board_path(board), id: "button-delete-#{board.id}", method: :delete, data: {confirm: t('defaults.message.d_confirm')} %>
  </li>
</ul>
  • ・回答例 # views/admin/boards/show.html.erb

    <% 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>
    

回答例の方が綺麗に書かれているし編集や削除ボタンもある。 レイアウトは参考程度にし、自分のも機能的には問題なかった。

・編集(edit)


これも元々の編集ファイルからコピーしたものにパスを変えただけの様なものになった

・自分の回答 # /views/admin/boards/edit.html.erb

<%= content_for(:title, @board.title) %>
  <h1><%= @board.title %></h1> # ↓ :adminを追加している
<%= form_with model: [:admin, @board], local: true do |f| %>
  <%= render 'layouts/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, rows: 10, 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>
  <div class="actions">
  <%= f.submit class:"btn btn-primary" %>
  </div>
<% end %>
  • ・回答例 # views/admin/boards/edit.html.erb

    <% 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, onchange: 'previewFileWithId(preview)', 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>
    

機能的には問題なくレイアウト違い

エラーメッセージのファイルは自分はlayoutsに入れてるのでエラーは出なかった

範囲検索(ransack)


ハッキリ言ってgithubの公式を見ただけで実装はできなかった。

activerecord-hackery/ransack

上のリンクの中にある Custom Predicates というところをクリックすると以下のコードがある

# config/initializers/ransack.rb

Ransack.configure do |config|
  config.add_predicate 'equals_diddly', # Name your predicate
    # What non-compound ARel predicate will it use? (eq, matches, etc)
    arel_predicate: 'eq',
    # Format incoming values as you see fit. (Default: Don't do formatting)
    formatter: proc { |v| "#{v}-diddly" }, # 
    # Validate a value. An "invalid" value won't be used in a search.
    # Below is default.
    validator: proc { |v| v.present? },
    # Should compounds be created? Will use the compound (any/all) version
    # of the arel_predicate to create a corresponding any/all version of
    # your predicate. (Default: true)
    compounds: true,
    # Force a specific column type for type-casting of supplied values.
    # (Default: use type from DB column)
    type: :string,
    # Use LOWER(column on database).
    # (Default: false)
    case_insensitive: true
end

f:id:michimo_10:20210607133249p:plain

ransack



この時は翻訳通してもイマイチ言ってることが分からず。

あと上部にコメントアウトしてる # config/initializers/ransack.rb ってなんなん?と思って

config/initializersの中を見たけどそんなファイル無いし

f:id:michimo_10:20210607133322p:plain



arel_predicateをgithub内で検索したり

activerecord-hackery/ransack

'ransack 日付 範囲' とか 'ransack arel_predicate' でググってたら既にまとめてくれている人がいました( ありがたや🙏 )

以下のページで'arel_predicate'と検索すると上の公式とかも出てくることろのちょい下に欲しいものがあった{第5章 044部分}

 

一応、上の中で出てきたlteqをgithub内で検索

activerecord-hackery/ransack

あ〜。。。 以下より小さいか等しい って意味ね。。。

かなり深いとこまで行かないとわかりませんでした。

逆に、 は以下より大きいか等しい となるとgteqに訳が充てられていたので、lteqgteqを使えば良さそうです(以下参照)

activerecord-hackery/ransack


ということで

猫Railsさんのページを見てransackファイルから作成します

config/initializers/ransack.rbが存在しない場合は追加(猫Railsから参照)

# config/initializers/ransack.rb
Ransack.configure do |config|
                       # 述語名
  config.add_predicate 'lteq_end_of_day',
                       # Arelの述語を指定。<=で検索したいからlteqを使うよ。
                       arel_predicate: 'lteq',
                       # インプットの整形。その日の終わりまでを検索対象に含めるよ。
                       formatter: proc { |v| v.end_of_day }
end

predicateは述語という意味です

ここまでで実装は完了。

以下は考察




検索のパーシャルに書かれていたものの中の下のピンク文字部分

<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>

これによって範囲を指定しているんですね。??

formatterの上のコメントアウト部分を翻訳すると

		# 入力される値を好きなようにフォーマットします。(デフォルト: フォーマットを行わない)
    formatter: proc { |v| "#{v}-diddly" },
    # 値を検証します。無効」な値は検索に使用されません。
    # 以下はデフォルトです。
    validator: proc { |v| v.present? },

ransack.rbにあるprocがオブジェクト化してくれているおかげ、と解釈しています

formatter: proc { |v| v.end_of_day }

更に、created_at_lteqだと0:00が基準になってしまうので上記でカスタマイズしたということです。

end_of_day はなんで使えるの?

end_of_dayはrailsのメソッドです 23:59:999999までを認識してくれます

Time




沼にハマったので先に言っておくと

中の動きを知る事も大事だがその機能を使うことが出来れば良い



極論言えば、「動けばいい」=͟͟͞͞( •̀д•́)



変数 | v | の v は、正直なんでも良いが、

よく見る| i | や | f | などの i は index の i で、f はform の f というようになっている

肝心のvは忘れた。すまん


以下、殴り書き

|v|はformatterの引数

formatter: proc { |v| v.〇〇} # の〇〇に代入すれば、lteqをend_of_dayに置き換えることができる。

add_predicateで検索の動作、formatterで入力値の変換

config.add_predicate 'to_age_lt',
  arel_predicate: 'gteq',
  formatter: -> (v) { (v.years.ago + 1.day).to_date },
  type: :integer,
  compounds: false

Ransack で年齢を検索する - Qiita

 

formatterで入力値を変換するので、

User.search(birthday_to_age_gteq: 18).result

これで検索する値が

18.years.ago + 1.day

18年前の今日、

2003/05/09

になる

rails cで見る

select * from users
where birthday >= '2003/05/09'