2012年10月25日

annotate(annotate_models)ってgemが地味に便利

Railsで開発してると、 「このModelのテーブルにどんなカラムを作ったっけ」 って思うことがよくある。

しかし、いちいちrails dbとかrails cとかで中身を見に行くのもだるい。

そんな時にannotateさんに頑張ってもらう。

導入は簡単。

とりあえず 前回のやつ に導入する。 まあgemなので、毎度おなじみGemfileに

gem 'annotate'

と追記して

$ bundle install

でも叩いておけば導入できる。

それから

$ bundle exec annotate

とでも叩けば

Annotated (1): Item

という感じで、何の情報を更新したかを教えてくれる。

で。実際にそのapp/models/item.rbファイルを見てみると

# == Schema Information
#
# Table name: items
#
#  id         :integer          not null, primary key
#  name       :string(255)
#  price      :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class Item < ActiveRecord::Base
  attr_accessible :name, :price
  strip_commas_from :price
end

こんな感じでコメントとしてファイル先頭にテーブルのスキーマ構造が投入されると。

さらにtest/unit/item_test.rbも

# == Schema Information
#
# Table name: items
#
#  id         :integer          not null, primary key
#  name       :string(255)
#  price      :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

require 'test_helper'

class ItemTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

こんな感じになってるし test/fixtures/items.ymlも

# == Schema Information
#
# Table name: items
#
#  id         :integer          not null, primary key
#  name       :string(255)
#  price      :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  name: MyString
  price: 1

two:
  name: MyString
  price: 1

こんな感じ。

あとはrspecを使ってる場合でも spec/models/item_spec.rbに書き込まれたはず。

あとはfactory_girlなどなどにも対応していたり、なんか色々オプションあるけど https://github.com/ctran/annotate\_models を見たら、英語だけどこのぐらいはわかるはずだから頑張ってね(丸投げ)

posted by 麦汁 at 23:23 | Comment(0) | TrackBack(0) | rails | このブログの読者になる | 更新情報をチェックする

2012年10月20日

Railsでinputの数値をカンマ区切りにするの巻。ついでに右寄せ。

ある日麦汁さんは出費をひたすら記録するだけのRailsアプリを作っていました。

彼は基本的にケチなので いつも一つの商品に対して1,000円に満たない程度の物しか購入していませんでした。

しかしある時、気前良く10万ほどの出費をしました。本当はしてませんけどしました。 そんな大きな出費をしたわけですから、記録しないわけにはいきません。

そんなわけで彼は自作のアプリに出費項目として登録しようとしました。

ところがなんということでしょうか。 10万なんていう数字は、あまりにも「0」の数が多過ぎて 麦汁さんの頭では 10000が正しいのか1000000が正しいのかがわかりません。

「せめて、せめてカンマ区切りにさえなってくれれば!!」

そんなわけで実装します。

環境は rails3.2.8で適当に

$ rails new cashbook

して、さらに適当に

rails g scaffold item name price:integer

したところから始めます。

とりあえずは、inputがカンマ区切りにさえなればいいので http://www.decorplanit.com/plugin/ から「autoNumeric」というjQueryのプラグインを拾って来ます。

普通にDLしたらautoNumeric-1.7.5.js.txtとかなってるので 拡張子の.txtを削除して 適当にRAILS_ROOT/app/assets/javascriptsにでも放り込みましょう。

で、そんでもって RAILS_ROOT/app/assets/javascripts/items.js.coffeeに 以下の記述を付け加えましょう。

$ ->
  $('input[type=number]').autoNumeric()

やったね! これで普通に数字を入れるだけで勝手に簡単にカンマ区切りになるよ!!

でもフォーカスを外したらケツに勝手に「.00」とか付けてくるよ! 許せないね!

ってことで記述を

$ ->
  $('input[type=number]').autoNumeric(aPad: false)

に変更。 これで余計な「.00」はついてこないよ! やったね! でも数値なんだから右寄せ表示がいいよね!

ってことで RAILS_ROOT/app/assets/stylesheets/items.css.scssを

input[type=number] {
  text-align: right;
}

に書き換え。 これでinputの表示にはとりあえず満足。

さあ。いざ登録しよう! Nameには「大きな出費」、 Priceには「100,000」(カンマは自動入力)と入力して、submitボタンを押そう!!

すると、なんということでしょう!

Item was successfully created.

Name: 大きな出費

Price: 100

なんで100が入っとんねん(怒) どうやらRailsがカンマは小数点だと思い込んでる模様。くそったれええええええ。

そこでおもむろにGemfileに以下の記述を追加しました。

gem "ar_strip_commas"

そうしてbundleコマンドを実行しました。 bundleを実行したのでサーバも当然再起動します。

さらにapp/models/item.rbを以下の様に書き換えました。

class Item < ActiveRecord::Base
  attr_accessible :name, :price
  strip_commas_from :price
end

よし。これで大丈夫のはず! というわけで改めて Nameには「大きな出費」 Priceには「100,000」(カンマは自動入力)と入力して update!

今度は

Item was successfully updated.

Name: 大きな出費

Price: 100000

む。登録したらカンマの区切りがなくて「0」の数がわからん!

まあここは大人しくapp/views/items/show.html.erbを以下のように編集。

<p id="notice"><%= notice %></p>

<p>
  <b>Name:</b>
  <%= @item.name %>
</p>

<p>
  <b>Price:</b>
  <%=number_with_delimiter @item.price %>
</p>

price のところに number_with_delimiter というhelperを適用しただけ。 あとindex.html.erbも同様にしておいた方がいいですねっと。

以上で、カンマがないと0の数が数えられないかわいそうな麦汁さんは救われました。 やったね!

ちなみにChromeでinput type='number'しているところにこれをやろうとしたら 4桁以上の数値を入力できなくなるので大分悲しい。

posted by 麦汁 at 22:35 | Comment(1) | TrackBack(0) | rails | このブログの読者になる | 更新情報をチェックする

2012年08月18日

Railsでstate_machineってgemの状態、イベントをDBから持って来て動的追加する調査

Rails3でstate_machineでもにゃりたいのでちょろっと調査。

もにゃりたいってのが、こう、状態をとりあえずDBにぶっこんで それをいじることでコードいじらずに状態を増やしたりできないかなあなどと。

ってことでとりあえずプロジェクトを立ち上げる。

$ rails new state_machine

別に名前はなんでもいいけどこれがわかりやすいでしょう。 proto_ とか付けてもよかったな。まあいいか。

で、プロジェクトの中に移動してGemfileを開いて

gem "state_machine"

を追加。 そして

$ bundle install

準備はとりあえずこんなもんでいいか。 この際rspecなんて、テストなんて要らぬ! (この手の調査系の記事って、見る側としてはテストが書かれてないのが悲しかったりするけどめんどい)

で、とりあえずなんか状態がありそうなモデルでも考える。

自動販売機でいいか。よく状態遷移マシンの題材になる気がするし。 他にもオブジェクト思考の話でわりとよく出る気がするな。 そっちだと、Animalクラスを継承したDogとCatを鳴かせたらワンとかニャーの方が印象強いけど。

で、「自動販売機」で調べたら「coin machine」って言葉もあるのな。それを使おう。

$ bundle exec rails g model CoinMachine state

めんどくさいからstateだけあればいいよね。 あとめんどくさいからmodelだけでいいよね。

さて、これ以上進む前にそもそもどういう状態があって どういう状態に遷移するのか考えようか。

電源をONにしたりOFFにしたりコイン入れたり何か買ったりする感じかな。 投入額については無視。めんどい。

ってことでstate_machineを使った場合のstaticな定義の書き方で書いてみると

state_machine :initial => "動いてない" do
  event "電源オン" do
    transition "動いてない" => "動いている"
  end

  event "コイン投入" do
    transition "動いている" => "金入ってる!!"
  end

  event "ボタン押す" do
    transition "金入ってる!!" => "動いている"
  end
  after_transition "金入ってる!!" => "動いている", :do => :sell_out

  event "電源オフ" do
    transition all => "動いてない"
  end
end

def sell_out
  puts "ほらよ。くれてやる。このいやしんぼめっ"
end

コイン投入して電源落としたら吸われてなくなる素敵仕様。 っていうか返却機能がない。気にしない。ガチャガチャと一緒だ。

あとはこの情報をDBから持ってくるから

$ bundle exec rails g model Transition event from to

って感じで適当にモデルでも作っておく。

あと地味にafter_transitionもあるので

$ bundle exec rails g model Hook timing from to do

timingは、beforeかafterかを入れるぐらい。 このあたりがあればきっとなんとか耐えられるだろう。

ってところで

$ bundle exec rails c

とか適当にやってみ……たかったけどTerminalから日本語打つのがなんかダメなので、 "transitions.rb"とかいうファイル名で

# -*- coding: utf-8 -*-
Transition.create!(event: "電源オン",   from: "動いてない",   to: "動いている")
Transition.create!(event: "コイン投入", from: "動いている",   to: "金入ってる!!")
Transition.create!(event: "ボタン押す", from: "金入ってる!!", to: "動いている")
Transition.create!(event: "電源オフ",   from: "all", to: "動いていない")

Hook.create!(timing: "after", from: "金入ってる!!", to: "動いている", do: "sell_out")

とか用意して

$ bundle exec rake db:migrate

しておいて

$ bundle exec rails runner transitions.rb

でTransitionとHookの投入。

それで、次はそれらを元に動的にCoinMachineのあれこれを定義するわけだ。

その中身は大体こんな感じ。

# -*- coding: utf-8 -*-
c = CoinMachine.new
p c
c.send("電源オン")
p c
c.send("コイン投入")
p c
c.send("ボタン押す")
p c
c.send("電源オフ")
p c

で、CoinMachineモデルの方も調整。

# -*- coding: utf-8 -*-
class CoinMachine < ActiveRecord::Base
  attr_accessible :state

  state_machine :initial => "動いてない" do
    Transition.all.each do |tr|
      event tr.event do
        transition (tr.from == "all" ? all : tr.from) => tr.to
      end
    end

    # after_transition
    Hook.find_all_by_timing("after").each do |hook|
      after_transition hook.from => hook.to, :do => hook.do
    end
  end

  def sell_out
    puts "ほらよ"
  end
end

allだけ特別扱いしているけど 基本的にTransitionのレコードからそのまんま動的にeventを書いて、 同じようにHookもやってるって感じ。

まあ https://github.com/pluginaweek/state\_machine/ の中の "Static / Dynamic definitions"の定義がごちゃごちゃしているから 適当に簡略化してみただけ。

でまあ実際に実行してみると

$ rails r state_machine_test.rb
#<CoinMachine id: nil, state: "動いてない", created_at: nil, updated_at: nil>
#<CoinMachine id: 6, state: "動いている", created_at: "2012-08-14 13:50:12", updated_at: "2012-08-14 13:50:12">
#<CoinMachine id: 6, state: "金入ってる!!", created_at: "2012-08-14 13:50:12", updated_at: "2012-08-14 13:50:12">
ほらよ。くれてやる。このいやしんぼめっ
#<CoinMachine id: 6, state: "動いている", created_at: "2012-08-14 13:50:12", updated_at: "2012-08-14 13:50:12">
#<CoinMachine id: 6, state: "動いていない", created_at: "2012-08-14 13:50:12", updated_at: "2012-08-14 13:50:12">

で、ここでDBに入れる利点として 後からeventだの状態だのを追加できるってのがあるよね。 っていうか当初の目的それだった。

ってことでやってみる。

# -*- coding: utf-8 -*-
c = CoinMachine.new
p c
c.send("電源オン")
p c
c.send("コイン投入")
p c
c.send("ボタン押す")
p c
Transition.create!(event: "蹴る", from: "all", to: "壊れた")
c.reload
c.send("蹴る")
p c
c.send("電源オフ")
p c

ちょっとまって。 このコードを毎回実行すると、 CoinMachineが増え続けるのはともかく Transitionのレコードも増え続けるぞ。 それは良くない。 いっそ一旦全部消して、生成から削除までするようなコード書こう。

# -*- coding: utf-8 -*-

ActiveRecord::Base.transaction do
  Transition.create!(event: "電源オン",   from: "動いてない",   to: "動いている")
  Transition.create!(event: "コイン投入", from: "動いている",   to: "金入ってる!!")
  Transition.create!(event: "ボタン押す", from: "金入ってる!!", to: "動いている")

  Transition.create!(event: "電源オフ",   from: "all", to: "動いてない")

  Hook.create!(timing: "after", from: "金入ってる!!", to: "動いている", do: "sell_out")

  c = CoinMachine.new
  p c
  c.send("電源オン")
  p c
  c.send("コイン投入")
  p c
  c.send("ボタン押す")
  p c
  Transition.create!(event: "蹴る", from: "all", to: "壊れた")
  c.reload
  c.send("蹴る")
  p c
  c.send("電源オフ")
  p c

  Transition.delete_all
  Hook.delete_all
  CoinMachine.delete_all
end

はうう。これってもしかしてUnitTestなりRSpecなり書いた方がよくね? まあいっか。実行。 これでイベントと状態が動的に追加できますように!

% rails r state_machine_test.rb
#<CoinMachine id: nil, state: "動いてない", created_at: nil, updated_at: nil>
#<CoinMachine id: 7, state: "動いている", created_at: "2012-08-14 13:51:14", updated_at: "2012-08-14 13:51:14">
#<CoinMachine id: 7, state: "金入ってる!!", created_at: "2012-08-14 13:51:14", updated_at: "2012-08-14 13:51:14">
ほらよ。くれてやる。このいやしんぼめっ
#<CoinMachine id: 8, state: "動いている", created_at: "2012-08-14 13:52:11", updated_at: "2012-08-14 13:52:11">
/Users/mugijiru/.rvm/gems/ruby-1.9.3-p125/gems/activemodel-3.2.6/lib/active_model/attribute_methods.rb:407:in `method_missing': undefined method `蹴る' for #<CoinMachine:0x007fdedbafa728> (NoMethodError
)        from /Users/mugijiru/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.6/lib/active_record/attribute_methods.rb:149:in `method_missing'
        from state_machine_test.rb:19:in `<top (required)>'
        from /Users/mugijiru/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.6/lib/rails/commands/runner.rb:51:in `eval'
        from /Users/mugijiru/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.6/lib/rails/commands/runner.rb:51:in `<top (required)>'
        from /Users/mugijiru/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.6/lib/rails/commands.rb:64:in `require'
        from /Users/mugijiru/.rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.6/lib/rails/commands.rb:64:in `<top (required)>'
        from script/rails:6:in `require'
        from script/rails:6:in `<main>'

うむ。やはり動的追加はできなかった。 そうだよなーRailsってClassは初めで全部読んでおっしまーいだもんなー。 だからGithubの例はあんなに複雑なんだな!

ってことで上のコードを書いて失敗したこともあって、 Githubの例についての理解が深まったので そっちに書き換えてみる。

まずはmachineメソッドを用意。

# -*- coding: utf-8 -*-
class CoinMachine < ActiveRecord::Base
  attr_accessible :state, :machine
  attr_writer :machine

  def machine
    coin_machine = self
    @machine ||= Machine.new(coin_machine, initial: "動いてない") do
      Transition.all.each do |tr|
        event tr.event do
          transition (tr.from == "all" ? all : tr.from) => tr.to
        end
      end

      # after_transition
      Hook.find_all_by_timing("after").each do |hook|
        after_transition hook.from => hook.to, :do => hook.do
      end

      after_transition do
        coin_machine.state = coin_machine.machine.state
        coin_machine.save!
      end
    end
  end
end

適当にGithubの例のと自分のコードをmergeしてみた感じ。

そしてMachineクラスを生成。 これはGithubのほぼそのままで、 hook のメソッドをとりあえずこっちに持ってきた。

class Machine
  def self.new(object, *args, &block)
    machine = Class.new do
      def definition
        self.class.state_machine
      end

      def sell_out
        puts "ほらよ。くれてやる。このいやしんぼめっ"
      end
    end
    machine.state_machine(*args, &block)
    machine.new
  end
end

で、この変更に伴いテスト用のコードも変更。

# -*- coding: utf-8 -*-

ActiveRecord::Base.transaction do
  Transition.create!(event: "電源オン",   from: "動いてない",   to: "動いている")
  Transition.create!(event: "コイン投入", from: "動いている",   to: "金入ってる!!")
  Transition.create!(event: "ボタン押す", from: "金入ってる!!", to: "動いている")

  Transition.create!(event: "電源オフ",   from: "all", to: "動いてない")

  Hook.create!(timing: "after", from: "金入ってる!!", to: "動いている", do: "sell_out")

  c = CoinMachine.new
  p c.state
  c.machine.send("電源オン")
  p c.state
  c.machine.send("コイン投入")
  p c.state
  c.machine.send("ボタン押す")
  p c.state
  Transition.create!(event: "蹴る", from: "all", to: "壊れた")
  c.machine = nil
  c.machine.send("蹴る")
  p c.state
  c.machine.send("電源オフ")
  p c.state

  Transition.delete_all
  Hook.delete_all
  CoinMachine.delete_all
end

ちなみにreloadじゃあダメだったのでこっそりとc.machineにnilを放り込んだりしている。

そして結果。

$ rails r state_machine_test.rb
nil
"動いている"
"金入ってる!!"
ほらよ。くれてやる。このいやしんぼめっ
"動いている"
"壊れた"
"動いてない"

初めにnilって言われるのが若干気にはなるがまあこんなもんでいいや。

posted by 麦汁 at 12:43 | Comment(0) | TrackBack(0) | rails | このブログの読者になる | 更新情報をチェックする