inuinu blog(開発用)

BOT @wagagun の開発ノウハウや、IT向け?の雑記ブログです。

【Ruby on Rails7】一覧画面に検索機能とソート機能を追加

この記事では、Ruby on Rails7の一覧表示に検索機能を追加し、さらに、ソート機能も追加します。

前回、

inuinu-tech.hatenablog.jp

で、ページネーションを追加しましたが、このままでは目的の情報を素早く拾えませんので、検索機能を追記します。

やりたいこと(その1:検索機能)

やりたいことは「販売履歴(Sale)に販売日付と顧客コードの検索機能をつける」。
とりあえず、この条件を実装してみます。

Gemfileに追記しインストール

まずはGemfileにページネーション系のライブラリを追加します。

# 検索機能
gem "ransack"

ansackが検索機能のgemライブラリになります。

bundle install

でインストールします。

コントローラーを変更する

app/controllers/sales_controller.rbを変更します。
(前回のkaminariの機能も加味しています。)

  # GET /sales or /sales.json
  def index
    @q = Sale.ransack(params[:q])
    @sales = @q.result(distinct: true).page(params[:page]).per(KAMINARI_PER).order(id: :desc)
  end

モデルに追記する

app/models/sale.rbに追記します。

  # ransackで検索可能な項目を指定する
  def self.ransackable_attributes(auth_object = nil)
    ["id", "sales_date", "customer_cd"]
  end

検索対象となる販売日付と顧客コードを記述します。

ビューに追記する

app/views/sales/index.html.hamlに追記します。

-# ransack用検索フォーム
= search_form_for @q, url: sales_path do |f|
  .grid.grid-cols-6.gap-3
    .col-span-2
      = f.search_field :sales_date_start, class: "", placeholder: "販売日付(前方一致)"
      = f.search_field :customer_cd_eq, class: "", placeholder: "顧客コード(完全一致)"

Bootstrap用のclassが紛れていますが、無視してください。
これでうまくいくと思います。

f.search_fieldについて

カラム名のあとに条件をつけるような形になっているようです。

github.com

上記のソースにある:sales_date_startは販売日付の前方一致、:customer_cd_eqは顧客コードの完全一致となります。

やりたいこと(その2:ソート機能)

ビューに追記する

app/views/sales/index.html.hamlに追記します。

%table.table.table-striped
  %thead
    %tr
      %th
        = sort_link(@q, :sales_date, "販売日付" )
      %th
        = sort_link(@q, :customer_cd, "顧客コード" )

こちらにもBootstrap用のclassが紛れていますが、無視してください。
これでうまくいくと思います。

参考:関連するレーブル含めて…の場合

こちらを参考にしてみてください。

qiita.com

【Ruby on Rails7】一覧画面を降順表示(件数切り捨て or ページネーション)

この記事では、Ruby on Rails7の一覧表示を降順に変更し、さらに表示件数を切り捨てる手法と、ページネーションで表示する手法を説明いたします。

やりたいこと(その1:一覧画面を降順表示し、n件で切り捨てたい)

コントローラーを変更する

例えば販売履歴(Sale)を最新から50件表示するようにします。
app/controllers/sales_controller.rbを変更します。

  # GET /sales or /sales.json
  def index
    @sales = Sale.order(id: :desc).limit(50)
  end

これだけです。

やりたいこと(その2:一覧画面を降順表示し、ページ処理を追加したい)

Gemfileに追記しインストール

まずはGemfileにページネーション系のライブラリを追加します。

# ページネーション
gem "kaminari"
gem 'bootstrap5-kaminari-views'

kaminariがページネーション用のgemライブラリになります。
Bootstrapを使用している場合は、それ用のライブラリも追記します。

bundle install

で、kaminariをインストールします。

コントローラーに追記

まず、app/controllers/application_controller.rbに追記します。

KAMINARI_PER = 50

もし、1ページの表示件数を他のページでも共通化したい場合は、app/controllers/application_controller.rbに定数を定義しておくと良いでしょう。

次に、app/controllers/sales_controller.rbを変更します。

  # GET /sales or /sales.json
  def index
    @sales = Sale.all.page(params[:page]).per(KAMINARI_PER).order(id: :desc)
  end

やりたいこと(その1)にあったlimit()は外し、上記を追記します。

ビューに追記

app/views/sales/index.html.hamlに以下を追記します。

%br
= paginate @sales, theme: 'bootstrap-5'

i18n関連ファイルに追記

上記でも実行できますが、ページ下部のボタン群を日本語化したいかと思います。
その場合は、下記を、任意のja.ymlに追記します。

ja:
  views:
    pagination:
      first: '最初'
      last: '最後'
      previous: '前'
      next: '次'
      truncate: '...'

これで実行し、確認します。

【Ruby on Rails7】cocoonを導入(その11:子の重複チェックを行いたい)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は子の値の重複チェックを行いたい場合の処理手順を説明します。

前回の続き。

inuinu-tech.hatenablog.jp

今回の処理範囲

もし、親側に新たにユニークキーを追加して、重複(一意性の)チェックを行いたい場合は、下記の記事をご参照ください。

inuinu-tech.hatenablog.jp

今回は子側の重複チェックになります。

実装手順

モデルに追記する

app/models/project.rbに次の行を追記します。

  # 子供側の重複チェックは親側で行う
  validate :check_uniqueness_tasks
(中略)
  private

  # 子供側の重複チェックは親側で行う(チェックロジック)
  def check_uniqueness_tasks
   # 明細のdescriptionの値を配列化する
   description = tasks.map(&:description)
   # uniqで重複値を除いた数と、明細の件数が一致しない場合にエラーとする
   errors.add(:base, 'descriptionが重複しています。') if description.uniq.length != description.length
  end

配列.uniqは重複した値を除いた配列を戻す関数ですので、それを利用します。

テーブルtasksにあるdescriptionがダブっている場合、description.uniq.lengthdescription.lengthが一致しないので、その場合、エラーを追加します。

:baseについて

errors.add():baseを使った場合、特定の項目にclassfield_with_errorsが挿入されないようです。

子の重複チェックで、特定の項目を赤くしたいということは、困難だと思いますので、そこは説明し、クライアントを納得させてください。

【Ruby on Rails7】id以外のカラムの重複チェックについて

この記事では、Ruby on Rails7の、id以外のカラムの重複チェックを行う手法について説明いたします。

何を行いたいか?

例えば、顧客マスターといったものを定義したい場合、Railsが勝手に採番してしまうidの他に、外部連携等を兼ねて、他のシステムでも使用している顧客コードを定義したいといったニーズがあるかと思います。

初期段階では、(他に稼働している)基幹システム等から、顧客マスタで使用する項目を抜き出し、移行すると思いますが、それ以降については、(二重メンテになるかもしれませんが)マスタメンテ画面にて変更を行う…といった仕様はあるかと思います。

(他に稼働している)基幹システムの顧客マスターが主で、Railsが従で、Rails側の変更はまずない場合は、夜間バッチ等で、毎日Rails側を置き換える手法が一般的かもしれません。
(idによる連携もあるので、結構大変かと思いますが…)
ただ、緊急時に備え、マスターの変更をかけたいというニーズはあるかもしれませんね。

その際に、顧客コードが重複しないか?といった一意性のバリデーションは必須条件になります。

どう実装するか?

マイグレーション

まずは、マイグレーションでindexを登録します。

bundle exec rails generate migration ChangeDuplicate

db/migrateディレクトリに作成されたファイルに以下を追記します。

●db/migrate/20231004004851_change_duplicate.rb(日付の部分はその都度異なります。)

    add_index :customers, :customer_cd, unique: true

以下のコマンドを実行します。

rake db:migrate

モデルの変更

さらに、モデルファイルapp/models/customer.rbに追記します。

  validates :customer_cd, presence: true, uniqueness: true

uniqueness: trueにて、顧客コードの重複チェックが行われます。

【Ruby on Rails7】hamlを導入するとscaffoldが以前のレイアウトになる

この記事では、Ruby on Rails7のhamlを使った、ちょっとした裏技をご紹介いたします。

私の開発の環境について

私の開発環境は以下の状況になっています。

Rails7以降は、scaffoldの一覧画面がDIVベースと聞いているが…

確かに、Rails7の導入当初に実行したscaffoldの一覧画面は、以前のようなTABLEではなくDIVベースになっていますね。

試しにサンプル的にscaffoldを実行し、CSSを何も当ててない状態で画面を見ると、その画面の長さにため息が出ます…

DIVの羅列もあまり好きじゃないし、hamlでどうスッキリするか、試してみようかと。

hamlをgem installした以降のscaffoldはTABLEベースに戻った

Gemfileにhamlを登録し、bundle installしscaffoldしてみると…あれ?TABLE調のレイアウトに戻ってる!

戻ったというよりも、hamlの場合での変更を失念しちゃったのかもしれませんね。
(あるいは、haml側が追いついていないか?)

ともかく、個人的には社内システムでのWeb開発がメインの人間なので、マスターメンテナンス系の一覧画面がTABLEベースなのは助かります。
しばらくは、hamlで行こうかと思います。

DIVベース(erb)のscaffoldを、TABLEベース(haml)に変更できないか?

モデル及びコントローラーについては差異はありませんし、コントローラーでerbといったキーワードは埋め込まれていませんので、ビューのみ(JSON以外)を変更します。

ビューのファイル名は拡張子以外は同一のはずですので、一番簡単なのは、カラムの数やカラム名が類似するモデルのビューのファイル(JSON以外)をコピーし、テーブル名(単数形・複数形の使い分けに注意)やカラム名を変更していく方法です。

これでうまくいくはずです。

【Ruby on Rails7】バリデーションエラー時のCSSを変更

この記事では、Ruby on Rails7のバリデーションエラー時のCSSを変更するための手順を紹介いたします。

バリデーションエラー時に赤くならない?

チュートリアル等ではバリデーションエラーで、エラーが赤く表示されますが…

あれ?
赤くならないな。
なんでだろう…?

よく見かける、バリデーションエラーでレイアウトが崩れた!もないね。

原因はBootstrapベースのデザインテンプレートでした

なるほど、気が利いたデザインテンプレートはRailsのこのあたりの対応もされているんですね。

新しいバージョンにはfield_with_errorsはないのかしら?と勘違いしていましたが、ちゃんと存在していました。

ただ、エラーの際にINPUTタグは赤く囲いたいなあ…と。

変更手順

私はscssを使用していますので、それを前提に。
app/assets/stylesheets/application.scssに以下を追記します。

/* バリデーションエラー時に赤で囲む */
#error_explanation {
  @extend .alert;
  @extend .alert-danger;
}

.field_with_errors {
  display: inline;
}

div.field_with_errors input, div.field_with_errors textarea {
  outline: none;
  border: 2px solid red;
}

#error_explanationはエラーメッセージの方ですね。こちらを赤(ピンク?)で囲みます。
.field_with_errorsはレイアウト崩れ対策です。私の方はすでに対策済みのデザインテンプレートを入れていますので、書いても書かなくても変わりませんが…
div.field_with_errors inputdiv.field_with_errors textareaについては、赤く囲うようにします。SELECTやラジオボタンチェックボックスについての記述がないのは、私がそのあたりの項目でバリデーションをかけていないから。必要な都度追加すれば良いと思っています。

is-invalidについて

Bootstrapではいつからかは忘れましたがis-invalidというバリデーションエラー用のclassが存在しています。

getbootstrap.jp

ですので、is-invalid@extendしても似たようなことができるかもしれません。

これをclassに挿入すると、INPUTタグの右端に「!」が表示されるのですが、これ、サイズがちっちゃいと、入力内容が切れてしまったりするので、私は使わないようにしています。

【Ruby on Rails7】cocoonを導入(その10:任意の行からコピーしたい)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は行追加時に、任意の行から値をコピーする手法を考えます。

前回の続き。

inuinu-tech.hatenablog.jp

前回は最終行からのコピーでしたが、今回は任意の行からコピーできるようにします。
(前回の実装が完了したという前提で話を進めていきます。)

ビュー側の変更

前回、親側のフォームに「□行コピーする」を追加しましたが、その横に、行位置を選択するセレクトボックスを追加します。
その場合、OPTIONタグは空欄にします。

●親の入力フォーム

= select :copy_select, :copy_option, []

JQueryを変更

OPTIONタグを描画

以下のようなファンクションを用意します。

function resetSelectOption(id, cnt) {
  let sl = document.getElementById(id);
  // nullの場合は処理中止(戻るボタン対応)
  if (sl == null) { return; }
  // 現状のoptionをすべてクリア
  while(sl.lastChild)
  {
    sl.removeChild(sl.lastChild);
  }

  // optionを追加
  for (let idx = 1; idx <= cnt; idx++) {
    let option = document.createElement("option");
    option.setAttribute("value", idx);
    option.innerHTML = idx + "件目";
    sl.appendChild(option);
    if (idx == cnt) {   //最終行
      option.setAttribute("selected", true);    // selectedの属性を付与
    }
  }
} // function

引数のidはSELECTタグのid。ビューの例ですと「copy_select_copy_option」といった値になっているはずです。
cntは、

inuinu-tech.hatenablog.jp

で求めた件数をセットします。

私の環境ですと、なぜか「戻る」ボタンでも発火したようなので、SELECTタグが存在しない場合は、早期リターンさせます。

次に、前回描画したOPTIONタグをすべて削除します。

さらに、件数分、OPTIONタグを追加します。
最終行にselectedをつけておくと良いでしょう。

どこから実行するか?

このファンクションは、下記のタイミングで呼び出します。
(cntを求めるファンクションも一緒に呼び出します。)

  • 初期処理(onLoadはエラーになるので、connect()の最初の方で呼び出す。)
  • cocoon:after-insert
  • cocoon:after-remove

コピー元を求める箇所を変更する

前回、

inuinu-tech.hatenablog.jp

の、

let copy_moto = flds_id.length - 2;

を変更します。

let copy_moto = document.getElementById(copy_select_id).value - 1; // コピー元

上記は、表示の行位置とvalueが一致している場合です。
(valueが行位置−1である場合は、マイナス1をする必要はありません。)

以上で、任意の行位置からコピーできるはずです。

ついでに、各行に行番号をつけたいのだが…

SELECTタグに「○件目」という選択肢を用意した都合上、「各明細に何行目か表す文字を表示したい」というニーズが生じるかもしれません。

行番号については、こちら

inuinu-tech.hatenablog.jp

の「1行目のみ削除ボタンを表示させない」にあるf.options[:child_index]が(0から始まる)明細行なので、これを+1したものを表示すれば、初期表示は可能です。

しかし、明細行を削除した場合に、番号を詰めてくれません。
また、明細行を追加した場合、その行番号エリアに「new_tasks」という、謎の文言が表示されます。

これらをJQueryで採番し直すのは、ちょっと億劫でしょうね。
(cocoon:after-insertで、SPANタグのidにユニークな値をセットする手法が、ちょっと思い浮かびませんので…)

ですので、避けたほうが良いでしょうね。

最後に

続編をアップしました。

inuinu-tech.hatenablog.jp