商品詳細表示機能② 販売価格表示も3桁区切りにしてみよう

商品出品機能実装過程で、販売手数料と販売利益はカンマ区切り表示になるようにしました。

ただ、販売価格についてはJavaScriptの絡みがあってうまく実装できずにいました。

商品詳細表示機能を実装していく中で、詳細画面に遷移した際に表示される情報としては、

  • 商品名
  • 商品画像
  • 販売価格
  • 出品者
  • カテゴリー
  • 商品の状態
  • 配送料の負担
  • 発送元の地域
  • 発送日の目安

上記の項目を表示させるようにします。

インスタンス変数.カラム名 でデータの取得が可能です。 販売価格を表示させようと思ったら、 @item.price で可能ではあるのですが、ここもどうにかしたらカンマ区切り表示させられるんじゃね? と思い立ち、調べてみることにしました。

すると、やっぱりあったー。

number_to_currency 通貨表示

number_to_currency は Rails フレームワークに組み込まれているヘルパーメソッドで、通貨をフォーマットするための便利なメソッドです。 これは RailsActionView ヘルパーの一部であり、HTML テンプレート内でよく使われます。

もうひとつ、数値を3桁区切り表示にさせる技があります。

to_sメソッド 数値を文字列オブジェクトに変換

to_s メソッドは基本的な Ruby メソッドで、オブジェクトを文字列に変換するためのものです。 例えば、数値を文字列に変換したい場合は、

12345678.to_s
 # => 12345678

このように記述します。 ここに引数で :delimited を渡すことによってカンマ区切り表示されます。

12345678.to_s(:delimited)
 # => 12,345,678

to_s メソッドを使って数値を文字列に変換することはできますが、あくまでも文字列。 さくっとカンマ表示できたらいいや、ってときはいいかもです。 ただ、通貨のフォーマットなどは手動で行う必要があります。

number_to_currency を使用すると、通貨のフォーマットや桁区切りなどが自動的に適用され、より便利になります。

わたしが採用した記述はこちら。

<%= number_to_currency(@item.price, unit: "¥", precision: 0, format: "%u %n" ) %>

このように記述すると、

こんな感じで表示されます。

オプション 説明
unit: "¥" 通貨マーク
precision: 0 小数点以下非表示(整数)
format: "%u %n" 円マークと数値の間に空白

precisionオプション を使わないと小数点以下が表示されてしまいます。 precision: 0 とすることで整数表示になります。

number_to_currency は通貨なので数値ですね。currendy ですからね。

もうこれは好みですね。お好きな方でどうぞって感じ。

ChatGPT先生によると、

# format: "%u %n" は、通貨の表示形式を指定するためのオプションです。具体的には、%u は通貨単位を表し、%n は数値部分を表します。 例えば、unit: "¥" を指定している場合、%u は "¥" を表示し、%n は数値を表示します。 このようなフォーマット指定を使うことで、通貨の単位と数値部分をカスタマイズすることができます。

とのこと。

◆例

number_to_currency(1234.56, unit: "¥", format: "%u %n") # 結果: "¥ 1,234.56"

この場合、"%u %n" により "¥" と数値がスペースで区切られて表示されます。 この部分を変更することで、通貨の表示形式をカスタマイズできます。

いろいろなアプローチができますね。

商品詳細表示機能① ~link_to~

link_to メソッドとは、リンクを作成するRailsのヘルパーメソッドです。 ビューファイルに記述できるメソッドになります。

以下は基本的な構文となります。

<%= link_to "テキスト", "リンク先のパス" %>

HTMLでリンクを作成するときはaタグを、Railsでリンクを作成するときは一般的に link_to を使います。 link_toメソッドを使うと、以下のようにaタグにコンパイルされます。

<a href="リンク先のパス">テキスト</a>

たとえばGoogleさんだと、

<%= link_to "Google","https://www.google.com/" %>

aタグにコンパイルされると、

<a href="https://www.google.com/">Google</a>

となります。

フリマアプリの場合は、複数の商品が存在し、それぞれにidが付与されます。 したがって、引数を指定するようになります。

rails routes でルーティングを確認すると、

少し見づらいですが、商品詳細ページに該当する item#showitem_path がリンク先のパスで items/id となっていることから idごとにページが存在することがわかります。

上記をふまえると、

<%= link_to item_path(item.id) do %>
  中略
<% end %>

このように記述することで目的の商品ページに遷移できるようになります。

do って何!?

上記コードのように do を記述することで、リンクの中に複数の要素やコンテンツを含めることができます。 do ~ end 内に含めたいものを記述します。

<%= link_to "Visit our site" do %>
  <strong>Click here!</strong>
<% end %>

上記の例では、"Visit our site" テキストをクリックすると、Click here! のような強調されたテキストが表示されるリンクが生成されます。

do ブロック内には、任意のHTMLやRubyコードを含めることができます。

こうすることで、柔軟なリンクの作成が可能となります。 画像を指定することも可能です。

表示させる順番を並び替えよう

フリマアプリの商品一覧表示機能の実装の条件の中に 『出品された日時が新しい順に表示されること』 とあります。

現状のままでは出品された日時の古い順になっています。 直近の出品商品が一番最後に表示される感じです。

これを表示の時に並び替えるのではなく、情報取得時に並び替える方法があります。

order メソッド

データベースから取り出すレコードを特定の順序で並べ替えたい場合は、orderメソッドが使えます。 今回は日時順に並び替えたいので、created_atカラムを指定します。

railsguides.jp

コントローラーに記述します。

def index
    @items = Item.all.order("created_at DESC")
end

これで出品された日時の新しい順に表示されるようになります。

補足情報 N+1問題を考える

N+1問題とは、アソシエーションを利用した場合に限り、データベースへのアクセス回数が多くなってしまう問題です。

userテーブルとitemテーブルがそれぞれ以下のようにアソシエーションで関連付けされているとします。

  • has_many :items
  • belongs_to :user

Item.all でデータを取得する場合は一度のアクセスで大丈夫なのですが、 トップページに一覧表示させる場合に出品者名も表示させるとすると、 userテーブルへもitemテーブルへアクセスするのと同じだけアクセスするようになります。

それぞれのデータ数が多ければ多いほどアクセス回数も多くなり、パフォーマンスの低下につながります。

この問題を解決するのに一役買うのが includesメソッド です。 このメソッドは、関連するモデルの情報をまとめて取得することができます。

def index
    @items = Item.inclues(:user).order("created_at DESC")
end

includesメソッド を使用することですべてのデータを取得できるので、allメソッドは省略することが可能です。

さあ、次は商品詳細表示機能の実装です。

たわいのないつぶやき

プログラミングとは全然関係ないんですけど、今、Tverで『ヤンキー君とメガネちゃん』が視聴可能です。 初めて見るんですけど、おもしろい!

何も考えずに見ることができるのと、よくよく見ていると出演者たちが名だたる顔ぶれで、それだけでも興味津々。 けっこう有名どころがが出ています。 見たことない―、という人はぜひ。

データあるなしでの条件分岐

present?

商品一覧表示機能の実装に取り組みます。 出品された商品データをトップページに一覧表示させる機能を実装していきます。 また、商品が何も出品されていな場合はダミー商品を表示させることとします。

まず、商品のインスタンス変数に何かしらのデータが入っている場合は、データをひとつずつ取り出して一覧表示をさせるようにします。

データがあるかないかを確認するため presentメソッド を、データが入っている場合はそれらをすべて表示できるように eachメソッド を使います。

● items_controller.rb

def index
  @items = Item.all
end

● index.html.erb

<% if @items.present? %>
 <% if @items.each do |item| %>
     中略
<% end %>

これでモデルにデータが保存されている場合は、商品情報がトップページに表示されるようになりました。 商品が何も出品されていない場合は else で分岐させ、何も商品が出品されていないときのみダミー商品が表示されることとします。 実装完了! Issue内には商品が売れてしまっている場合は『soldout』と表示させる条件も入っていますが、 現時点では商品購入機能の実装がまだなのでステイです。

presentメソッドとあわせて紹介されていたメソッドがあるので備忘録として記載しておきます。

nil?メソッド
変数.nil?

nilとは一言で言うと「何も存在しない」という意味です。 変数自体が存在しないかどうかを判定します。

empty?メソッド
変数.empty?

変数の値が空白かどうかを判定します。

blank?メソッド
変数.blank?

変数そのものが存在しないか、変数の値が空白かどうかを判定します。 nil?メソッドとempty?メソッドの併せ技みたいなもののようです。

なので、else とせずとも blank? でも同様の結果が得られます。

<% if @items.blank? %>

これもまた然り。

こちらに実験要素強めの記載あり

ブログを綴っていくにあたって、メモに題材を残しているのをふと見ると

  • valueとparseFloat()とparseInt() 整数どっちがいいんだ?
  • textContentと.innerHTMLどっちがいいんだ

と書き記してある。 一体何を思って残したのだろう? このとき、私は何をしようとしていたのだろう?

メモを残すにももう少し工夫が必要かもしれない...

次回は表示順の並び替えについて綴ります。

数字のカンマ表示について

前回の商品出品機能実装完了から、気がつけば2週間も経ってしまっていた... 気を取り直して続きに取り掛かろう!と再び実装を進めるつもりだったのですが、 Issueに記載している 『販売手数料と販売利益におけるカンマでの区切り表示は必須ではない』

に目がとまり、販売手数料と販売利益はカンマ表示できてる。 でも入力する販売価格のカンマ表示ができてない。 ここもできるのでは!? と気になりだしたら試したくなるわたし。

きっと方法があるはず、と調べてみると

number_to_currency(数値, オプション={})

数値を通過のフォーマットに変換させるものとしてこのようなものがあるらしい。

railsdoc.com

オプションとして :delimiter を使うとカンマ表示ができるようだ。

でも、一体どこにコードを書くのか... ChatGPTに聞いてみる。

すると、

number_to_currency メソッドは、Railsのヘルパーメソッドであり、通常は ActionView::Helpers::NumberHelper モジュールに含まれています。このメソッドを使用するためには、ActionView::Helpers::NumberHelper モジュールを ItemsHelper モジュールに取り込む必要があります。

とのこと。

# app/helpers/items_helper.rb
module ItemsHelper
  include ActionView::Helpers::NumberHelper

  def number_to_currency(price)
    super(price, delimiter: ',')
  end
end

このようなコードを紹介される。

早速やってみるがこれだけでは表示されない。

もう少しChatGPTとやりとりを続けてみると、このヘルパーメソッドを使用するためには、関連するビューでヘルパーモジュールを読み込む必要があるらしい。 例えばの提案コードがこちら。

# app/controllers/items_controller.rb
class ItemsController < ApplicationController
  helper ItemsHelper

  # コントローラのコード
end

同じように "helper ItemHelper" をわたしのコードにも追記してみる。 でもまだ表示されない。

さらに続くやりとり。

初心者脳のわたしの質問にイラッとした様子のChatGPT。 新たに二択の提案をしてくれました。

  • ビューファイルの適切な場所で "number_to_currency" を使用する

  • JavaScriptを使用して動的にフォーマットを変更する

ビューファイルに "number_to_currency" を組み込んでみるとエラーで真っ赤になってしまったので後者で攻めてみることにする。きっと適切な場所ではなかったんだろう。

window.addEventListener('turbo:load', () => {
  const itemPrice = document.getElementById("item-price");
  itemPrice.addEventListener('input', () => {
    const addTaxPrice = Math.floor(itemPrice.value * 0.1);
    const profit = itemPrice.value - addTaxPrice;

    const addTaxPriceElement = document.getElementById("add-tax-price");
    const profitElement = document.getElementById("profit");

    // itemPriceの値をtoLocaleStringを使ってフォーマット
    itemPrice.value = Number(itemPrice.value).toLocaleString('ja-JP');

    addTaxPriceElement.textContent = addTaxPrice.toLocaleString('ja-JP');
    profitElement.textContent = profit.toLocaleString('ja-JP');
  });
});

このようにファイルを修正。 すると、数字の1,000まではうまくいく。でも、10,000にすると "NaN"と表示されてしまう。 うーーーーん、わからんーーーーー。

と、ちょっとした好奇心から数時間消費するという悪夢。 いまだ解決せず。

どなたか、詳しい方がいらっしゃったら教えてください。

気分転換に次のIssueに進みたいと思います。

Ajaxで手数料と利益の計算をするの巻③ inオプション

numericality

このヘルパーは、属性に数値のみが使われていることをバリデーションします。 デフォルトでは、整数値または浮動小数点数値にマッチします。 これらの冒頭に符号がある場合もマッチします。

1つ前の記事でもふれましたが、範囲指定のところ。 300から9,999,999の間のみ許す。 私が採用したのは

こちらなんですが、どうやら in: 300. .9_999_999 でも可能らしい。

github.com

いろいろ検索してたら見つけました。 沼ってた時に採用していたのですが、エラーメッセージがうまくいってなくてエラー出まくりだったので戻しました。

でも、エラーが解決した今。 いけるんじゃね!?と思って試してみたらいけましたー。 めっちゃDRY。でも戻しておく。

Railsガイドでは numericality のオプションとしては greater_thanless_than になってる。 inオプションinclusionexclusion のヘルパーみたい。 きっとどっちでもいいんだろうけど。

商品出品機能実装完了!

次は商品一覧表示機能です。

Ajaxで手数料と利益の計算をするの巻② モデル単体テスト

JavaScript 実装が完了し、最後の詰めでモデルのバリデーションをかけ直しました。

販売価格は、

  • ¥300~¥9,999,999の間のみ保存可能であること。
  • 価格は半角数値のみ保存可能であること。

こちらは numericality でいけそう。

さっそく実装します。

こんな感じ。

値として数値のみを許す

only_integer: true

¥300~¥9,999,999の間のみ許可

greater_than_or_equal_to: 300,less_than_or_equal_to: 9_999_999

これで販売価格のバリデーション設定は完了! ちなみに、数字の間にあるアンダーバーは表示上数字を見やすくするためのもので、 データを取得しても『1_000』であれば『1000』となります。 Web上でカンマ区切りで表示させることもできます。

そして、画像で示してある通り、わたしはmessageオプションを使いました。 それが沼の始まりでした。 というか、バリデーションの仕組みを理解していなかったのがいけなかったんです。

たとえば、 only_integer: true でバリデーションをかけると、 デフォルトのエラーメッセージは「must be an integer」なんです。

同様に、 greater_than の場合は「must be greater than %{count}」、 less_than の場合は「must be less than %{count}」なんです。

Active Record バリデーション - Railsガイド

そこをもうみんな一緒でいいよねー、ってことで「is invalid」 無効です、にしてたのにテストファイルとモデルファイルのエラーメッセージの統合性が取れていなかったものだから エラー、エラーのオンパレードで、エラーメッセージの始まりは大文字でなければいけない、とか 「can't be blank」なのに「is valid」になっているとかでひとつずつエラーを解消していったのですが、 最後に残ったのが販売価格のところ。

販売価格は値が空ではいけない、半角数字でなければいけない、指定の範囲でなければいけない、と複数のバリデーションがかかっており ChatGPTに聞いてみたり検索したりする中でいろいろな意見があるわけです。 それらすべてを試してみて、最終的にこちら。

require 'rails_helper'

RSpec.describe Item, type: :model do
  before do
    @item = FactoryBot.build(:item)
  end
  
  describe '商品情報登録' do
    context '商品情報が登録できるとき' do
      it '正常に登録できるとき' do
        expect(@item).to be_valid
      end
    end
  
    context '商品情報が登録できないとき' do
      it 'itemが空では登録できない' do
        @item.name = ''
        @item.valid?
        expect(@item.errors.full_messages).to include "Name can't be blank"
      end
      it '商品画像が空では登録できない' do
        @item.image = nil
        @item.valid?
        expect(@item.errors.full_messages).to include "Image can't be blank"
      end
      it 'カテゴリーが空では登録できない' do
        @item.category_id = ''
        @item.valid?
        expect(@item.errors.full_messages).to include "Category can't be blank"
      end
      it '商品の状態が空では登録できない' do
        @item.condition_id = ''
        @item.valid?
        expect(@item.errors.full_messages).to include "Condition can't be blank"
      end
      it '配送料の負担情報が空では登録できない' do
        @item.postage_type_id = ''
        @item.valid?
        expect(@item.errors.full_messages).to include "Postage type can't be blank"
      end      
      it '発送元の地域情報が空では登録できない' do
        @item.prefecture_id = ''
        @item.valid?
        expect(@item.errors.full_messages).to include "Prefecture can't be blank"
      end
      it '発送までの日数情報が空では登録できない' do
        @item.shipping_time_id = ''
        @item.valid?
        expect(@item.errors.full_messages).to include "Shipping time can't be blank"
      end
      it '価格情報が空では登録できない' do
        @item.price = nil
        @item.valid?
        expect(@item.errors.full_messages).to include "Price can't be blank"
      end
      it '価格は¥299以下の場合は登録できない' do
        @item.price = '299'
        @item.valid?
        expect(@item.errors.full_messages).to include "Price is invalid"
      end
      it '価格は¥1,000,000以上の場合は登録できない' do
        @item.price = '10000000'
        @item.valid?
        expect(@item.errors.full_messages).to include "Price is invalid"
      end
      it '価格は半角数値でなければ登録できない' do
        @item.price = 'ああああ'
        @item.valid?
        expect(@item.errors.full_messages).to include "Price is invalid"
      end
    end
  end
end

やっと通りましたー!

エラーメッセージも適当に入れたらだめなのね、とかデフォルト値があるのね、とか めっちゃ勉強になりました。 さらに続く。