おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

【Rails】英語を日本語化! Gem i18n

Ruby on Railsで英語を簡単に日本語表示してくれるGem「i18n」の紹介です。

事前準備

Railsアプリ全体の言語設定を日本語に変更します。

# config/application.rb
config.i18n.default_locale = :ja

Gemfilerails-i18nを設定してbundle installします。

# Gemfile
gem 'rails-i18n'

ベースとなる日本語辞書ファイルをダウンロードします。

config/locales/ja.ymlを作成しrails-i18n/ja.yml at master · svenfuchs/rails-i18n · GitHubの内容を全てコピペします。

# config/locales/ja.yml
---
ja:
  activerecord:
    errors:
      messages:
        record_invalid: 'バリデーションに失敗しました: %{errors}'
        restrict_dependent_destroy:
          has_one: "%{record}が存在しているので削除できません"
          has_many: "%{record}が存在しているので削除できません"
  date:
    abbr_day_names:
    - 日
    - 月
    - 火
    - 水
    - 木
    - 金
    - 土
.
.
.

これで完了です。

基本的な使い方

先ほどのja.ymlに追加します。

ja:
  views:
    hoges:
      hello: 'こんにちは'

この状態で、view側のファイルで= t('views.hoges.hello')と書くと「こんにちは」と表示されていると思います。

色々な使い方

モデル名を日本語表示

# ja.yml
ja:
  activerecord:
    models:
      user: "ユーザー"
# コンソール
> User.model_name.human
=> "ユーザー"

カラム名を日本語表示

# ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: 名前
# コンソール
> User.human_attribute_name :name
=> "名前"

日付フォーマットを操る

日付を色々なパターンで表示する

ja.ymlformatsが定義されていて、それを利用すると状況に応じて色々なフォーマットで日付を表示することが可能です。

# ja.yml
la:
  date:
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"
# 指定なし
> I18n.l date
=> "2019年09月26日(木)"

# :short
> I18n.l date, format: :short
=> "9月26日(木)"

# :long
> I18n.l date, format: :long
=> "2019年09月26日(木)"

# viewで使うとき
<%= l date, format: :short %>

曜日部分を日本語化する

# ja.yml
ja:
  data:
    abbr_day_names:
    - 日
    - 月
    - 火
    - 水
    - 木
    - 金
    - 土

元々曜日に関する日本語が定義されていますので、この状態で下記のように曜日部分を日本語表示することが可能です。

# コンソール
> date = Date.today
=> Thu, 26 Sep 2019

> date.strftime("%Y/%m/%d(%a)")
=> "2019/09/26(Thu)"

> date.strftime("%Y/%m/%d(#{I18n.t('date.abbr_day_names')[date.wday]})")
=> "2019/09/26(木)"

%w(日 月 火)って定義しておいてDate.today.wdayでindexを取得してやる方法が調べるとよく出てきますが、こっちの方が余計な記述も不要でスッキリですね。

【Rails】簡単にformが作成できるsimple_form

Ruby on Railsで登録機能や更新機能を作る際に必要なform。

このformの作成を簡単に作成することが出来る便利なライブラリ「simple_form gem」のご紹介。

simple_formで作った問い合わせform画面のイメージ
simple_formで作った問い合わせform画面のイメージ

インストール手順

Gemfileに追加して、bundle installする。

gem 'simple_form'

モデルの作成

generateコマンドを使ってモデルを作ります。

$ bundle exec rails g model hoge name:string age:integer gender:string birthday:date email:string

rake db:migrateしてDBにテーブルを作成します。

$ bundle exec rake db:migrate

こちらで作成したhogesテーブルです。

 Table "public.hoges"
   Column   |            Type             | Collation | Nullable |              Default              
------------+-----------------------------+-----------+----------+-----------------------------------
 id         | bigint                      |           | not null | nextval('hoges_id_seq'::regclass)
 name       | character varying           |           |          | 
 age        | integer                     |           |          | 
 gender     | character varying           |           |          | 
 birthday   | date                        |           |          | 
 email      | character varying           |           |          | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "hoges_pkey" PRIMARY KEY, btree (id)

コントローラーの作成

newアクションはモデルをnewしてオブジェクトする。

class HogesController < ApplicationController
  def index
  end

  def new
    @hoge = Hoge.new
  end
end

createアクションは今回は割愛します...

画面の作成

form_tagの代わりにsimple_form_forを使います。

ざっくり使い方

# haml
.hoges__new
  .hoges__new-title
    %h2
      新規登録画面
    %p
      ご依頼・お問い合わせなどございましたら、下記よりお気軽にご連絡ください。
  .hoges__new-form
    = simple_form_for(@hoge, url: hoges_path) do |f|
      = f.input :name
      = f.input :age
      = f.input :birthday
      .hoges__new-form-btn
        = f.submit

# html
<div class="hoges__new">
  <div class="hoges__new-title">
    <h2> 新規登録画面 </h2>
    <p> ご依頼・お問い合わせなどございましたら、下記よりお気軽にご連絡ください。 </p>
  </div>
  <div class="hoges__new-form">
    <form class="simple_form new_hoge" id="new_hoge" novalidate="novalidate" action="/hoges" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓"><input type="hidden" name="authenticity_token" value="6VTA5orkwF0ot26n+iZKp6+OeokxeTOxTnAm79HSX1RFx9LM9OMnevbi1HP7YoCl3cjJGrAa24rGhf6NFIRb0Q==">
      <div class="form-group string optional hoge_name"><label class="control-label string optional" for="hoge_name">お名前</label><input class="form-control string optional" type="text" name="hoge[name]" id="hoge_name"></div>
      <div class="form-group integer optional hoge_age"><label class="control-label integer optional" for="hoge_age">ご年齢</label><input class="form-control numeric integer optional" type="number" step="1" name="hoge[age]" id="hoge_age"></div>
      <div class="form-group date optional hoge_birthday"><label class="control-label date optional" for="hoge_birthday_1i">誕生日</label>
        <select id="hoge_birthday_1i" name="hoge[birthday(1i)]" class="form-control date optional">
          <option value="2014">2014</option>
          <option value="2015">2015</option>
          <option value="2016">2016</option>
          <option value="2017">2017</option>
          <option value="2018">2018</option>
          <option value="2019" selected="selected">2019</option>
          <option value="2020">2020</option>
          <option value="2021">2021</option>
          <option value="2022">2022</option>
          <option value="2023">2023</option>
          <option value="2024">2024</option>
        </select> 
        <select id="hoge_birthday_2i" name="hoge[birthday(2i)]" class="form-control date optional">
          <option value="1">1月</option>
          <option value="2">2月</option>
          <option value="3">3月</option>
          <option value="4">4月</option>
          <option value="5">5月</option>
          <option value="6">6月</option>
          <option value="7">7月</option>
          <option value="8">8月</option>
          <option value="9" selected="selected">9月</option>
          <option value="10">10月</option>
          <option value="11">11月</option>
          <option value="12">12月</option>
        </select> 
        <select id="hoge_birthday_3i" name="hoge[birthday(3i)]" class="form-control date optional">
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
          <option value="4">4</option>
          <option value="5">5</option>
          <option value="6">6</option>
          <option value="7">7</option>
          <option value="8">8</option>
          <option value="9">9</option>
          <option value="10">10</option>
          <option value="11">11</option>
          <option value="12">12</option>
          <option value="13">13</option>
          <option value="14">14</option>
          <option value="15">15</option>
          <option value="16">16</option>
          <option value="17">17</option>
          <option value="18">18</option>
          <option value="19">19</option>
          <option value="20">20</option>
          <option value="21">21</option>
          <option value="22">22</option>
          <option value="23">23</option>
          <option value="24">24</option>
          <option value="25" selected="selected">25</option>
          <option value="26">26</option>
          <option value="27">27</option>
          <option value="28">28</option>
          <option value="29">29</option>
          <option value="30">30</option>
          <option value="31">31</option>
        </select> 
      </div>
      <div class="hoges__new-form-btn"> <input type="submit" name="commit" value="登録する" data-disable-with="登録する"></div>
    </form>
  </div>
</div>

formの種類

simple_formの便利なところでカラムの型に応じてformの種類の自動で判断し生成してくれます。

  • string型: テキストフィールド
  • integer型: 数値フィールド
  • date型: 年月日のセレクトボックス
  • boolean型: チェックボックス
# テキストフィールドの表示
= f.input :name

# ラジオボタンの表示
= f.input :inquiry_type, as: :radio_buttons

# セレクトボタンの表示
class User
  GENDERS = %w(男性 女性 性別無回答).freeze
end
= f.input :gender, collection: User::GENDERS, label: false, hint: '感想レビューに表示されます'

# hidden項目
= f.input :not_delivery, as: :hidden, input_html: { value: false }

# ボタンの表示
= f.submit class: 'btn btn-primary', data: {disable_with: t('text.disable_with')}

便利な機能

simple_formで作った問い合わせform画面のイメージ
simple_formで作った問い合わせform画面のイメージ

# ラベル名の変更(label)
= f.input :email, label: false
= f.input :email, label: 'メールアドレス'

# 未入力時に表示する文字列(placeholder)
= f.input :email, placeholder: 'メールアドレス'

# classやidを指定する(class, id)
= f.input :email, class: 'form-control'

# 備考欄を表示する(hint)
= f.input :tel, hint: '半角数字、-(ハイフン)'

# セレクトボタンで初期選択を空にする
= f.input :birthday, start_year: Date.current.year - 100, end_year: Date.current.year - 10, include_blank: true
 ※初期文字列を表示したい時は文字列を設定して下さい。

日本語化

ラベル名の部分など所々デフォルトだと英語表示されますが、これらは「i18n」というgemを利用すると日本語表示することが可能です。

# config/locales/ja.yml
ja:
  activerecord:
    attributes:
      hoge:
        name: 'お名前'
        age: 'ご年齢'
        birthday: '誕生日'

こんな感じでja.ymlファイルを作成しモデル名とカラム名に紐づく日本語を定義しておくと自動で日本語表示されます。 色々なことが出来るので、これはまた別の記事でまとめたいと思います。

【Rails】バリデーションをまとめてみる

データを登録する際に利用するフォームですが、条件に応じて必須だったり制限したりする仕組みを「バリデーション」といいます。 Ruby on Railsでは以下のような感じで設定することができます。

class Hoge < ApplicationRecord
  validates :name, presence: true
end
# validationがない場合
> hoge = Hoge.new
=> #<Hoge:0x00007f803eabca78 id: nil, name: "", age: 0, gender: "man", birthday: Mon, 01 Jan 1900, created_at: nil, updated_at: nil>
> hoge.save
   (0.3ms)  BEGIN
  Hoge Create (2.8ms)  INSERT INTO "hoges" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2019-09-24 11:19:16.757343"], ["updated_at", "2019-09-24 11:19:16.757343"]]
   (0.8ms)  COMMIT
=> true

# validationがある場合
> hoge = Hoge.new
=> #<Hoge:0x00007fc554e8fd48 id: nil, name: "", age: 0, gender: "man", birthday: Mon, 01 Jan 1900, created_at: nil, updated_at: nil>
[2] pry(main)> hoge.save
   (0.3ms)  BEGIN
   (0.3ms)  ROLLBACK
=> false
[3] pry(main)> hoge.errors.full_messages
=> ["Name入力してください。"]

このようにvalidationを設定することで、条件が満たされていないとデータが保存されないように出来ます。

今回は入力が必須の設定であるpresenceを使ったので、最後のエラー文に"Name入力してください。"といったエラーが自動的に設定されます。 これらの設定が色々とありますので、そちらをまとめていきたいと思います!

今回使うテーブルはこちら↓↓

                                         Table "public.hoges"
   Column    |            Type             | Collation | Nullable |              Default              
-------------+-----------------------------+-----------+----------+-----------------------------------
 id          | bigint                      |           | not null | nextval('hoges_id_seq'::regclass)
 name        | character varying           |           | not null | ''::character varying
 age         | integer                     |           | not null | 0
 gender      | character varying           |           | not null | 'man'::character varying
 birthday    | date                        |           | not null | '1900-01-01'::date
 email       | character varying           |           | not null | 
 postal_code | character varying           |           | not null | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "hoges_pkey" PRIMARY KEY, btree (id)

バリデーションが実行されるタイミング

バリデーションが実行されるタイミングは色々とあるのですが、例えばこの二つです!

  • hoge.save
  • hoge.valid?

saveはその名の通りデータを保存する際に使い、valid?は保存せずにvalidationが成功するかどうかを判断する時に使います。

また、save(validate: false)とするとvalidationしないようにすることも可能です。

他のものは、また別の機会でまとめてみたいと思います。

バリデーションのエラー

バリデーションに失敗すると勝手にエラーに関する情報が保存されます。

> hoge.errors
=> #<ActiveModel::Errors:0x00007fe8e626a940
 @base=#<Hoge:0x00007fe8ca962638 id: nil, name: "ai", age: 0, gender: "man", birthday: Mon, 01 Jan 1900, created_at: nil, updated_at: nil>,
 @details={:name=>[{:error=>:too_short, :count=>10}]},
 @messages={:name=>["は10文字以上で入力してください。"]}>

> hoge.errors.full_messages
=> ["Nameは10文字以上で入力してください。"]

errorsとすると、モデルの詳細な情報が取得できて、errors.full_messagesとするとエラーメッセージだけ取得できます。 使われ方としては、このfull_messagesを画面側で表示させてエラー内容を表示させたりします。

ここでNameという形で英語で表示されてしまってますが、これもi18nという仕組みを使うことで日本語表示させることも可能です。 が、ここでは説明しません。別の機会に使い方をまとめたいと思います。

バリデーションの種類

空かどうか(presence)

validates :name, presence: true

> hoge = Hoge.new
=> #<Hoge:0x00007fc556fe6618 id: nil, name: "", age: 0, gender: "man", birthday: Mon, 01 Jan 1900, created_at: nil, updated_at: nil>
> hoge.valid?
=> false

文字数(length)

# 最小の数
validates :name, length: { minimum: 10 }

# 最大の数
validates :name, length: { maximum: 20 }

# 数の範囲
validates :name, length: { in: 10..20 }

> hoge.name = 'ai'
=> "ai"
> hoge.name.length
=> 2
> hoge.valid?
hoge=> false
> hoge.errors.full_messages
=> ["Nameは10文字以上で入力してください。"

> hoge.name = 'aiueokakikukekosashisuseso'
=> "aiueokakikukekosashisuseso"
> hoge.name.length
=> 26
> hoge.valid?
=> false
> hoge.errors.full_messages
=> ["Nameは20文字以内で入力してください。"]

ユニーク(uniqueness)

# name単体でユニーク
validates :name, uniqueness: true

# name/emailでユニーク
validates :name, uniqueness: { scope: [:email] }

> Hoge.all
  Hoge Load (0.5ms)  SELECT "hoges".* FROM "hoges"
=> [#<Hoge:0x00007fe60d860628 id: 1, name: "", age: 0, gender: "man", birthday: Mon, 01 Jan 1900, created_at: Tue, 24 Sep 2019 20:19:16 JST +09:00, updated_at: Tue, 24 Sep 2019 20:19:16 JST +09:00>,
 #<Hoge:0x00007fe60d831c38 id: 2, name: "opiyo", age: 30, gender: "man", birthday: Mon, 01 Jan 1900, created_at: Tue, 24 Sep 2019 20:58:20 JST +09:00, updated_at: Tue, 24 Sep 2019 20:58:20 JST +09:00>]
> hogehoge = Hoge.new
=> #<Hoge:0x00007fe604662938 id: nil, name: "", age: 0, gender: "man", birthday: Mon, 01 Jan 1900, created_at: nil, updated_at: nil>
> hogehoge.name = 'opiyo'
=> "opiyo"
> hogehoge.age = 20
=> 20
> hogehoge.save
   (0.3ms)  BEGIN
  Hoge Exists (0.4ms)  SELECT  1 AS one FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
=> false
> hogehoge.errors.full_messages
=> ["Nameはすでに存在します。"]

数値のみ(numericality)

validates :age, numericality: true

> hoge.age = 'opiyo'
=> "opiyo"
> hoge.valid?
=> false
> hoge.errors.full_messages
=> ["Ageは数値で入力してください。"]

正規表現(format)

validates :postal_code, format: { with: /\A[0-9]{3}-[0-9]{4}\z/ }

> hoge.postal_code = '1234-567'
=> "1234-567"
> hoge.valid?
=> false
> hoge.errors.full_messages
=> ["Postal codeは不正な値です。"]

条件付き(if:、on:)

# カラムの値やメソッドなどで条件を設定する時
validates :name, presence: true, if: Proc.new { age < 20 }

# アクションに応じて設定する時
validates :postal_code, format: { with: /\A[0-9]{3}-[0-9]{4}\z/ }, on: update

> hoge.age = 18
=> 18
> hoge.save
   (0.3ms)  BEGIN
   (0.3ms)  ROLLBACK
=> false
> hoge.errors.full_messages
=> ["Name入力してください。"]
> hoge.age = 30
=> 30
> hoge.valid?
=> true

バリデーションを自分で作る(validate、with_options)

# メソッドを作ってチェックする
validate :enter_name_if_minor

def enter_name_if_minor
  if self.age < 20
    errors[:base] << '未成年の場合は名前を入力してください!'
  end
end

> hoge.age = 18
=> 18
[3] pry(main)> hoge.valid?
=> false
[4] pry(main)> hoge.errors.full_messages
=> ["Age未成年の場合は名前を入力してください!"]

# 別のバリデーションを作る(メール送る処理の時だけ使うバリデーション)
with_options on: :send_mail do
  validates :email, presence: true
end

> hoge.valid?(:send_mail)
=> false
> hoge.errors.full_messages
=> ["Email入力してください。"]
> hoge.valid?
=> true

# saveを使うときは`context`を使う
> hoge.save(context: :send_mail)

その他

その他にも複雑なvalidationが必要な場合に色々なgemがあったりします。

  • phonelib(電話番号のvalidation)
  • email_validator(メールアドレスのvalidation)

上記で紹介した正規表現(:format)を使えば実現できるのですが、電話番号やメールアドレスは結構考えることが多く大変です。 こういう場合は、だいたいgemがあるのでそれを使えばokだと思います!

Rails最高ですね。

【Rails】調整さんのようなカレンダーを表示する

調整さんのようなカレンダーを作ると表示する内容が固定されていればtableタグなどで出来るが、 DBに保存された内容だとどうhtmlを組めば分からない。


調整さんってご存知ですか?

調整さん - 簡単スケジュール調整、出欠管理ツール

ちょっとした打合せとかする時に皆の予定を確認するのにめちゃくちゃ便利な調整さん。

これと似たような画面をrailsでどうやって作るのかな〜と思い試してみました。

調整さんを使ったカレンダー画像
調整さんを使ったカレンダー画像

こちらが完成イメージです。それではカレンダーを表示してみましょう!

この記事を読めば以下2つが分かるようになります。

調整さんを実現するモデル

キャラクターモデル

# character.rb
class Character < ApplicationRecord
  extend Enumerize

  has_many :deathblows
  has_many :character_magics, dependent: :destroy
  has_many :magics, through: :character_magics
  accepts_nested_attributes_for :deathblows, allow_destroy: true

  enumerize :name, in: [:terra, :celes, :locke, :sabin]
  enumerize :sex, in: [:male, :female]

  validates :name, presence: true
  validates :sex, presence: true

  def build_default_deathblows
    (self.deathblows.count..7).each do
      self.deathblows.build
    end
  end
end

魔法モデル

# magic.rb
class Magic < ApplicationRecord
  has_many :character_magics, dependent: :destroy
  has_many :characters, through: :character_magics
  
  validates :name, presence: true
end

キャラクター魔法モデル

# character_magic.rb
class CharacterMagic < ApplicationRecord
  belongs_to :character
  belongs_to :magic
end

調整さんのようなカレンダー表示

ルーティングや、コントラーラーは今回割愛しますが、画面の作り方になります!

view

イメージとしてはキャラクターごとに1行にまとめ、それを横に並べていく感じです。

# index.haml
%h1 調整さんのようなカレンダー
.tyousei__calendars
  %ul.tyousei__calendars-body
    %li.tyousei__calendars-item
      %p -
      - @magics.each do |magic|
        %p
          = magic.name
    - @characters.each do |character|
      %li.tyousei__calendars-item
        %p
          = character.name_text
        - @magics.each do |magic|
          %p
            - if character.character_magics.find_by(magic_id: magic.id).present?
              ●

css

横並びはdisplay: flexを使えばもう簡単にできますね。

後は、文字に応じて高さや幅が変わってしまうので一つ一つのセルに指定してやればokです!

.tyousei__calendars-body
  display: flex
.tyousei__calendars
  list-style: none
.tyousei__calendars-item
  list-style: none
  width: 100px
  text-align: center
  &:first-child
    font-weight: 700
  p
    height: 30px
    &:first-child
      font-weight: 700



そして、完成したカレンダーがこちら!

Railsで作ったカレンダー
Railsで作ったカレンダー

【Rails】DBに保存する前のデータを削除する方法は`delete`

オブジェクトをnewしたけど、条件に応じて内容が空になることがわかった。

この状態でsaveしちゃうとvalidationに引っかかるしオブジェクトを削除したい。

が、DB保存前なのでActiveRecordで削除する感じでdeleteはできないしどうすれば...

そんな疑問に今日は応えていきます!

今日のお題はこちら

  • DB保存前のオブジェクトを削除する方法

今日親に紐づく子のデータを複数同時に保存する処理を作っていたんですが、 formが空の場合カラムにnilがせっとされたまま送られてくるのでvalidationに引っかかるし不要なデータなので削除した。

だけど、DBに保存前だしこれどうやって削除するんだ?ってなって調べてみて解決したので、それを共有できればと思います。

モデルの構成

親モデル: character(キャラクター) 子モデル: deathblow(必殺技)

ER図

# character.rb
class Character < ApplicationRecord
  extend Enumerize

  has_many :deathblows
  accepts_nested_attributes_for :deathblows, allow_destroy: true

  enumerize :sex, in: [:male, :female]

  validates :name, presence: true
  validates :sex, presence: true
end
# deathblow.rb
class Deathblow < ApplicationRecord
  belongs_to :character
  
  validates :name, presence: true
  validates :acquisition_level, presence: true
end

DB保存前のオブジェクトを削除する方法

結論としては、親モデル.子モデル達.delete(削除する子モデル)でokです! つまり

character.deathblows(deathblow)

では実際に画面のキャプチャー/コード/ログを見ながら解説して行きます。

必殺技登録: edit

マッシュのレベルが6になりましたので、魔列車で大活躍の「オーラキャノン」を習得させてあげましょう!w

# characters_controller.rb
class CharactersController < ApplicationController
  def edit
    @character = Character.find params[:id]
    @character.build_default_deathblows
  end

  def update
    @character = Character.find params[:id]
    @character.update(character_params)
  end

  private
  def character_params
    params.require(:character).permit(:name, :sex, deathblows_attributes: [:name, :acquisition_level])
  end
end
# character.rb
class Character < ApplicationRecord
  def build_default_deathblows
    (self.deathblows.count..7).each do
      self.deathblows.build
    end
  end
end
# edit.haml
%h1 キャラクターの登録
= simple_form_for(@character, url: characters_path) do |f|
  = f.input :name
  = f.input :sex, collections: Character.sex.values.delete_if {|v| v == 'male'}
  = f.fields_for :deathblows do |cf|
    = cf.input :name
    = cf.input :acquisition_level
  = f.submit

f:id:opiyotan:20190723232659p:plain

必殺技登録: update

マッシュは、まだレベル6なので「オーラキャノン」までしか覚えられません。

だけど、「むげんとうぶ」まで覚える事を考慮してform自体は最大の8つまで準備しているせいで 無駄なデータが送られきてしまいます。

f:id:opiyotan:20190723233202p:plain

ここで、今日の本題。こいつらも一緒にsaveしちゃうとvalidationに引っかかり弾かれちゃいます。

なので、データを削除します!

  def update
    @character = Character.find params[:id]
    @character.attributes = character_params
    @character.build_default_deathblows.each do |deathblow|
      @character.build_default_deathblows.delete(deathblow) unless deathblow.valid?
    end
    @character.update(character_params)
  end
  • attributesでデータを親モデルにセット
  • 子モデルをeachで1つ1つデータをチェックする
  • validしてvalidationが問題ないかチェック
  • エラーならdeleteする

こんな感じです。

まとめ

本来ならば、動的にformが追加できるようにするべきです。 削除しまっているので、validationで引っかかった場合は、editに戻りますがbuildする処理を通らないので formが消えっぱなしになりますので。

では最後に、もう一度おさらいです。

# 親モデル.子モデル達.delete(子モデル)@character.build_default_deathblows.delete(deathblow)

ps. やってて気づきましたが、deleteする処理はmodelでbefore_save使えばコントローラー側は2行で済むかもですね。

  def update
    @character = Character.find params[:id]
    @character.update(character_params)
  end

【Rails】enumerizeで定義したフォームのセレクトタグをアクション別に変える

今日やりたかったのは、新規作成時と編集時で同一カラムなんだけどセレクトタグに表示する内容を変えたいってのがあった。

編集の場合は色々な条件があって変更されると困るってのがあったので、悩んでたんだけど結構簡単に出来たのでその方法を紹介します。

新規作成時のview

# new.haml
%h1 キャラクターの登録

= simple_form_for(@character, url: characters_path) do |f|
  = f.input :name
  = f.input :sex

セレクトタグの表示内容画像
セレクトタグの表示内容画像

編集時のview

第二引数にあたる、collectionsにセレクトタグに表示する内容を設定することができます。

# edit.haml
%h1 キャラクターの登録

= simple_form_for(@character, url: characters_path) do |f|
  = f.input :name
  = f.input :sex, collections: Character.sex.values.delete_if {|v| v == 'male'}

コンソールで実行するとこんな感じで、条件に応じて合致したものを削除してくれるってのがdelete_ifになります。

Character.sex.values => ["male", "female"]

Character.sex.values.delete_if {|v| v == 'male'} => ["female"]

delete_ifで削除したセレクトタグの画像
delete_ifで削除したセレクトタグの画像

これだけだなのですが、環境周りとかも紹介させてもらえればと思います。

環境/使い方紹介

Gemfileの設定

先ずはenumが使えるように、gem enumrizeを設定します。

# Gemfile
gem 'enumerize'

modelの設定

次は対象のカラムに対してenumerizeの設定をします。

# character.rb
class Character < ApplicationRecord
  extend Enumerize

  enumerize :sex, in: [:male, :female]
  
  validates :name, presence: true
  validates :sex, presence: true
end

viewの設定

formの生成はsimple_formを使います。こちらの使い方はまた別の機会に。 enumを設定をしたのが:sexですが、この記述だけで勝手にセレクトタグを生成してくれます!クソ楽チン。

# new.haml
%h1 キャラクターの登録

= simple_form_for(@character, url: characters_path) do |f|
  = f.input :name
  = f.input :sex

日本語化の対応

ymlにenumerizeを定義してあげて、対象モデル名とカラム名に対して日本語を設定してやれば表示上は日本語で、データ保存時は英語でってのが簡単に実現できる

# ja.yml
ja:
  enumerize:
    character:
      sex:
        male: '男性'
        female: '女性'

【Rails】営業日を考量して日付を操るGem business_time

回答期限に応じて何かを処理するって時に営業日を考慮したい時があり色々調べているとスンバラシイですね。

business_timeっていうgemがあったので簡単な使い方を紹介します!

導入方法

Gemfileを設定してbundle installします。

# Gemfile
gem 'business_time'

使い方

bundle exec rails cで色々検証してみました。

日付の操作

現在日付

Time.current.to_s => "2019-07-09 21:48:18 +0900"

1日後

1.business_day.from_now.to_s => "2019-07-10 21:48:23 +0900"

1日前

1.business_day.ago.to_s => "2019-07-08 21:55:19 +0900"

1時間前

1.business_hour.ago.to_s => "2019-07-09 20:54:58 +0900"

営業日チェック

今日が営業日か

Date.today.to_s => "2019-07-09"

Date.today.workday? => true

7/9が営業日か

Date.parse("2019-07-09").workday? => true

7/13(土)が営業日か

Date.parse("2019-07-13").workday? => false

自分の誕生日が休日か?

my_birthday = Date.new(2019, 8, 4) => Sun, 04 Aug 2019

my_birthday.to_s => "2019-08-04"

my_birthday.workday? => false

注意点

Timezoneはきちんと設定しましょう!

TimeZoneはきちんとtokyoにしておかないと計算がおかしくなるので、注意です!

# confing/application.rb

class Application < Rails::Application
  config.time_zone = 'Asia/Tokyo'
end

きちんと設定されていれば、今の日付が表示されると思います。

Time.current.to_s => "2019-07-09 21:54:34 +0900"

1.business_hour.from_now.to_s => "2019-07-10 10:53:59 +0900"

が、この設定がないとおかしくなります。

Time.current.to_s => "2019-07-09 13:08:15 UTC"

1.business_hour.from_now.to_s => "2019-07-09 14:08:19 UTC"

営業時間の判定もされる

普通にやってると2日後とかになってあれっなると思います。

Time.current.to_s => "2019-07-09 22:09:30 +0900"

1.business_hour.from_now.to_s => "2019-07-10 11:00:00 +0900"

この場合は、1時間後が表示されるはずです。つまり2019-07-09 23:09:30 +0900ですね。

が、このgemは営業時間を持っているのでその時間も考慮して計算しているので気をつけましょう。

こちらの設定については、こちらのコマンドを実行することでymlが作られそこに定義されています。

$ rails generate business_time:config
business_time:
  beginning_of_workday: 10:00 am
  end_of_workday: 10:00 pm
  holidays:
    - Jan 01, 2010
    - July 4th, 2010
    - December 25th, 2010
  work_week:
    - mon
    - tue
    - wed
    - thu
    - fri
  • beginning_of_workday: 開始時間
  • end_of_workday: 終了時間

【Rails】データが1件でもあるかどうかチェックするには`Model.exists?`

Ruby on Railsでデータベースにデータが存在するかどうかチェックしたい場合があると思います。

  • そもそもデータがあるかどうか知りたい時
  • ログインしたユーザーが管理者ユーザーがどうかをチェックしたい

そんな時はModel.exists?()を使いましょうって話です。

データが存在するかチェックする

> User.exists?
  User Exists (3.0ms)  SELECT  1 AS one FROM "users" LIMIT $1  [["LIMIT", 1]]
=> true

Userモデルにデータが1件でも存在するかのチェックです。

条件付きで存在をチェックする

> User.exists?(id: [1, 2, 3], name: 'opiyo', role: 'admin')
  User Exists (0.6ms)  SELECT  1 AS one FROM "users" WHERE "users"."id" IN ($1, $2, $3) AND "users"."name" = $4 AND "users"."role" = $5 LIMIT $6  [["id", 1], ["id", 2], ["id", 3], ["name", "opiyo"], ["role", "admin"], ["LIMIT", 1]]
=> false

Userモデルから管理者ユーザーを探すような条件を渡してます。条件は何個でもつけれますし、配列も渡せちゃいます。

find_byも同じ使い方出来るじゃね?

データを1件だけ取得するのに便利なのがModel.find_by()だと思うのですが、exists?と同じような使い方ができますね。

> User.find_by(id: [1, 2, 3], name: 'opiyo', role: 'admin')
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3) AND "users"."name" = $4 AND "users"."role" = $5 LIMIT $6  [["id", 1], ["id", 2], ["id", 3], ["name", "opiyo"], ["role", "admin"], ["LIMIT", 1]]
=> nil

さっきのと全く同じ条件ですが戻り値が違いますね。今回はnilです。

取得できた場合はデータが1件取れますね。

> User.find_by(name: 'opiyo', role: 'admin')
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = $1 AND "users"."role" = $2 LIMIT $3  [["name", "opiyo"], ["role", "admin"], ["LIMIT", 1]]
=> #<User id: 175, created_at: "2018-05-28 02:59:58", updated_at: "2019-06-18 05:42:19", name: "opiyo", role: "admin">

データも欲しいけど存在チェックもしたいっていう贅沢な事をする場合は、こんな書き方で叶えることが出来るので便利!

> if user = User.find_by(name: 'opiyo', role: 'admin')
*   puts user.name
*   puts user.role
* end  
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = $1 AND "users"."role" = $2 LIMIT $3  [["name", "opiyo"], ["role", "admin"], ["LIMIT", 1]]
opiyo
admin

取得に失敗した場合はuser = nilになりますが、nilfalseなのでif文に入りません。

> if user = User.find_by(name: 'opiyoopiyo', role: 'admin')
*   puts user.name  
*   puts user.role  
* end  
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = $1 AND "users"."role" = $2 LIMIT $3  [["name", "opiyoopiyo"], ["role", "admin"], ["LIMIT", 1]]
=> nil

さっき出力された、opiyoadminが表示されませんね。

参考サイト

railsdoc.com

rake db:migrationがどこまで実行されてるのか確認したり戻したり

あるデータベースのカラムを追加したくと色々やっていたら、今どんな状況なのか良く分からなくなったので整理する。

migration実行状況確認

$ rake db:migrate:status
Running via Spring preloader in process 1796

database: hogehoge

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170509072520  Create events
   up     20170510082334  Add details to event # Statusがup = 実行済みの状態

migrationの実行を元に戻す

$ rake db:rollback
Running via Spring preloader in process 1817
== 20170510082334 AddDetailsToEvent: reverting ================================
== 20170510082334 AddDetailsToEvent: reverted (0.0399s) =======================

$ rake db:migrate:status
Running via Spring preloader in process 1832

database: hogehoge

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170509072520  Create events
  down    20170510082334  Add details to event # Statusがdownになった

migrationファイルを削除する

$ rake db:migrate:status
Running via Spring preloader in process 1852

database: hogehoge

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170509072520  Create events
   # Add details to eventの行がなくなった

Googleフォームで回答数と回答された合算を表示する方法

前回も似たような話題を取り上げたのですが、Googleフォームを使って回答数を表示させる方法です。

opiyotan.hatenablog.com

ざっくり手順紹介

  1. 数値を入れる項目を作成
  2. スクリプトエディタを起動する
  3. 入力された数をカウントして説明欄に表示するスクリプト作成
  4. トリガーを設定

スクリプト部分はこんな感じですね。

form.setDescription('申し込んだ子どもの数: ' + sum_child_count + '人'); 

本当にコレだけなんですが、これじゃ分からないのでもう少しだけ細かく。

回答された数を表示する

単純に回答された数を表示するだけであれば、非常に簡単です。

function resultCount() {
  resultCount = 0;
  
  var form = FormApp.getActiveForm(); //アクティブフォームを取得
  resultCount = form.getResponses().length; // 全回答内容を取得
  
  // 説明欄に申し込み済み子供の数を表示する
  form.setDescription('申込まれた数: ' + resultCount + '人'); 
}

Googleフォームで申込まれた件数を表示する画像
Googleフォームで申込まれた件数を表示する画像
これで実行すると

めちゃくちゃ簡単ですね。

アンケート項目の数値を合算した数を表示する

今回僕がやってたのが、まさにこれなんですが少し複雑になります。

ですが、やってることは上と同じです。

function myFunction() {
  //子どもの数上限を設定
  var LIMIT_COUNT = 4;
  sumChildCount = 0;
  
  var form = FormApp.getActiveForm(); //アクティブフォームを取得
  var formResponses = form.getResponses(); // 全回答内容を取得

  for (var i = 0; i < formResponses.length; i++) {
   var formResponse = formResponses[i]; // 回答ひとつ分を取得
   var itemResponses = formResponse.getItemResponses(); // 質問項目を取得
    
   for (var j = 0; j < itemResponses.length; j++) { // 回答内容をひとつずつチェック
     var itemResponse = itemResponses[j];
     var question = itemResponse.getItem().getTitle();
     var answer = itemResponse.getResponse();
     
     // 申込み数カウント
     if( question == '子どもの数' ){
       sumChildCount += Number(answer);
     }
    }
  }
  
  // 説明欄に申し込み済み子供の数を表示する
  form.setDescription('申し込んだ子どもの数: ' + sumChildCount + '人'); 
}

申込まれた子供の数を表示する画像
申込まれた子供の数を表示する画像

ざっと説明すると、さっきのと違うのが回答数ではなくて子供の数はに答えてくれた総数を表示している点ですね。

なので、回答された子供の数を見つけ出して変数に合算して行ってる感じです。

for (var i = 0; i < formResponses.length; i++) {

全ての回答内容を先ずは取得して、for文でグルグル回します

for (var j = 0; j < itemResponses.length; j++) { // 回答内容をひとつずつチェック

1件の回答に対する全ての項目を更にグルグル回します

var question = itemResponse.getItem().getTitle();

項目のタイトルを取得します

var answer = itemResponse.getResponse();

回答された数を取得します

if( question == '子どもの数' ){
  sumChildCount += Number(answer);
}

さっき取得したタイトルを使って条件文を作り、そこに回答された数を合算していく処理です。

あとは、説明欄に合算した値を渡してあげれば出来上がりーです。

まとめ

結構色々できるなーってのが正直、面白いですね。

だけど意外と情報が少ないので、ハマると結構苦労します。

今ハマっているがTwitterでも呟いちゃったんですが

単純な回答数ならばアドオン「formLimiter」とかで対処できそうなのですが、今回みたいにGAS側で数を計算してその結果から判断するみたいなことができないのかな〜と。

知っている方がいれば是非!教えていただけると嬉しいです。

ということで話が少し脱線しましたが、以上でございます。