ラベル Rails3 の投稿を表示しています。 すべての投稿を表示
ラベル Rails3 の投稿を表示しています。 すべての投稿を表示

2013年11月16日土曜日

Railsで多階層のテーブル結合をfindするサンプル


上図のようにテーブルが多階層(3階層以上)で結合されている場合に、ActiveRecord の findメソッドでconditionsを指定する方法を考えてみる。
rails generate model gun name:string
rails generate model shidan gun:references name:string
rails generate model rentai shidan:references name:string
rake db:migrate
サンプルデータも投入しておく。
rails c
Gun.create(:name=>'1-gun')
Gun.create(:name=>'2-gun')
Shidan.create(:gun_id=>1, :name=>'1-shidan')
Shidan.create(:gun_id=>2, :name=>'2-shidan')
Rentai.create(:shidan_id=>1, :name=>'1-rentai')
Rentai.create(:shidan_id=>2, :name=>'2-rentai')
この状態で、2階層であれば、findメソッドで下記のようなconditionsを指定できる。
Rentai.all(:include => :shidan, :conditions => ["shidans.id = ?", 2])
しかし、なんとなく直感的に3階層の場合のconditionsを下記のように設定しても、うまくいかない。
Rentai.all(:include => :shidan.gun, :conditions => ["shidans.gun.id = ?", 2])

NoMethodError: undefined method `gun' for :shidan:Symbol
includeオプションについては、下記のページに記述があったので、 

Ruby on Railsあれこれ/findメソッドの:includeオプションの書き方
http://winter-tail.sakura.ne.jp/pukiwiki/index.php?Ruby%20on%20Rails%A4%A2%A4%EC%A4%B3%A4%EC%2Ffind%A5%E1%A5%BD%A5%C3%A5%C9%A4%CE%A1%A7include%A5%AA%A5%D7%A5%B7%A5%E7%A5%F3%A4%CE%BD%F1%A4%AD%CA%FD

下記のようにincludeを書き換えてみたものの、やはりダメである。
Rentai.all(:include => {:shidan => :gun}, :conditions => ["shidans.gun.id = ?", 2])

ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: shidans.gun.id: SELECT "rentais"."id" AS t0_r0, "rentais"."shidan_id" AS t0_r1, "rentais"."name" AS t0_r2, "rentais"."created_at" AS t0_r3, "rentais"."updated_at" AS t0_r4, "shidans"."id" AS t1_r0, "shidans"."gun_id" AS t1_r1, "shidans"."name" AS t1_r2, "shidans"."created_at" AS t1_r3, "shidans"."updated_at" AS t1_r4, "guns"."id" AS t2_r0, "guns"."name" AS t2_r1, "guns"."created_at" AS t2_r2, "guns"."updated_at" AS t2_r3 FROM "rentais" LEFT OUTER JOIN "shidans" ON "shidans"."id" = "rentais"."shidan_id" LEFT OUTER JOIN "guns" ON "guns"."id" = "shidans"."gun_id" WHERE (shidans.gun.id = 2)
ダメではあったものの、出力されたSQLには有益な情報が含まれているので、余計な個所を削って人間にとって見やすいように成形してみる。
SELECT 
  rentais.id,
  rentais.shidan_id,
  rentais.name,
  rentais.created_at,
  rentais.updated_at, 
  shidans.id,
  shidans.gun_id,
  shidans.name,
  shidans.created_at,
  shidans.updated_at,
  guns.id,
  guns.name,
  guns.created_at,
  guns.updated_at
FROM rentais
LEFT OUTER JOIN shidans ON shidans.id = rentais.shidan_id
LEFT OUTER JOIN guns ON guns.id = shidans.gun_id
WHERE (shidans.gun.id = 2)
結局のところ、conditonsではSQLのWHERE句の内容を切り出しているに過ぎず、rubyだからといってオブジェクト間の階層を難しく意識せず、SQLに忠実に、"guns.id"で指定すれば良いようである。

下記のように書き直すことで、意図した検索結果を得ることができた。
Rentai.all(:include => {:shidan => :gun}, :conditions => ["guns.id = ?", 2])

=> [#<Rentai id: 2, shidan_id: 2, name: "2-rentai", created_at: "2013-11-16 07:12:21", updated_at: "2013-11-16 07:12:21">]

2013年8月9日金曜日

RspecでFactory Methodパターンをテストするサンプル


工場の工員が製品を製造するというFactory Methodパターンのテストを考えてみる。
工員と製品は工場に所属していて、工員が製品を製造すると、工場の在庫(製品)が増える。

まずは必要なモデルを生成する。
rails g model koujou name:string
rails g model kouin koujou:references name:string
rails g model seihin koujou:references seihin_num:integer
rake db:migrate
工場モデルに工員、製品間の1対多の関係を設定しておく。
class Koujou < ActiveRecord::Base
  has_many :kouins
  has_many :seihins
end
次に、工員モデルに製品の製造メソッドを追加する。
class Kouin < ActiveRecord::Base
  belongs_to :koujou

  def seizou
    seihin = Seihin.new
    seihin.koujou_id = koujou_id
    max_seihin_num = Seihin.maximum(:seihin_num)
    if max_seihin_num.blank?
      seihin.seihin_num = 1
    else
      seihin.seihin_num = max_seihin_num + 1
    end
    return seihin
  end
end
下記のような感じでRspecのテストコードを記述してみる。
テストコード内でsaveしてもDBにはcommitされないので、製造後の製品の配列のサイズの確認は常に1で問題ない。
require 'spec_helper'

describe Koujou do
  it '製品製造後の在庫(製品)増加の確認.' do
    koujou = Koujou.new
    koujou.name = 'テスト'
    koujou.save

    p koujou.seihins

    kouin = Kouin.new
    kouin.koujou_id = koujou.id
    kouin.name = '浜松浜子'
    kouin.save

    seihin = kouin.seizou
    seihin.save

    p koujou.seihins

    expect(koujou).to have(1).seihins
  end
end
ところが、テストが通らない。
製造後の製品の配列のサイズは0のままである。
bundle exec rspec spec/models/koujou_spec.rb
Rack::File headers parameter replaces cache_control after Rack 1.5.
[]
[]
 [31mF [0m

Failures:

  1) Koujou 製品製造後の在庫(製品)増加の確認.
      [31mFailure/Error: [0m  [31mexpect(koujou).to have(1).seihins [0m
        [31mexpected 1 seihins, got 0 [0m
 [36m     # ./spec/models/koujou_spec.rb:23:in `block (2 levels) in <top (requir
ed)>' [0m

Finished in 0.53903 seconds
 [31m1 example, 1 failure [0m

Failed examples:

 [31mrspec ./spec/models/koujou_spec.rb:6 [0m  [36m# Koujou 製品製造後の在庫(製
品)増加の確認. [0m

Randomized with seed 17099
よく考えてみると前回の記事と関連した問題である。
子モデルの値を参照する毎にDBから最新の値を取得しているのではなく、インスタンスに変更が加わらない限りは一度DBから参照した値をそのまま保持しているので、最初に参照した時点で配列が空になっている状態をその後も返しているわけである。

下記のように、途中で子モデルの値を確認する余計なコードを外して再度実行すると、チェックの時点で初めてDBから値を取得するのでテストが通るようになる。
require 'spec_helper'

describe Koujou do
  it '製品製造後の在庫(製品)増加の確認.' do
    koujou = Koujou.new
    koujou.name = 'テスト'
    koujou.save

    kouin = Kouin.new
    kouin.koujou_id = koujou.id
    kouin.name = '浜松浜子'
    kouin.save

    seihin = kouin.seizou
    seihin.save

    expect(koujou).to have(1).seihins
  end
end
bundle exec rspec spec/models/koujou_spec.rb
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [32m. [0m

Finished in 0.52803 seconds
 [32m1 example, 0 failures [0m

Randomized with seed 58919
なんとなく気持ち悪いので、下記のように、expectの内部で製造を行った上で、チェックする時点で明示的にDBから値を取得するようにして、差分の確認はbyというマッチャを使うと安定しそうだ。
require 'spec_helper'

describe Koujou do
  it '製品製造後の在庫(製品)増加の確認.' do
    koujou = Koujou.new
    koujou.name = 'テスト'
    koujou.save

    kouin = Kouin.new
    kouin.koujou_id = koujou.id
    kouin.name = '浜松浜子'
    kouin.save

    expect {
      seihin = kouin.seizou
      seihin.save
    }.to change{Koujou.find(koujou.id).seihins.size}.by(1)
  end
end
bundle exec rspec spec/models/koujou_spec.rb
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [32m. [0m

Finished in 0.59903 seconds
 [32m1 example, 0 failures [0m

Randomized with seed 65120

2013年8月4日日曜日

after_saveイベントで作成した子モデルが反映されるタイミング


上手のような親子関係(鮭とイクラ)を持つモデルにおいて、親モデルを作成した時点で子モデルを自動的に作成するようにしたい。

Shakeモデルは前回の記事で既に作成しているので、子となるIkuraモデルを今回新規に作成する。(実行結果は省略)
rails g model ikura shake:references name:string
rake db:migrate
親モデルShakeのafter_saveというイベントに合わせて子クラスIkuraを生成するように実装してみる。
class Shake < ActiveRecord::Base
  has_many :ikuras

  after_save :create_ikuras

  private

  def create_ikuras
    if ikuras.blank?
      ikura1 = Ikura.new
      ikura1.shake_id = id
      ikura1.name = 'First Ikura'
      ikura1.save
    end
  end
end
さっそくコンソールで実行してみても、子モデルが生成されていない。
Loading development environment (Rails 3.2.1)
irb(main):001:0> shake = Shake.new
=> #<Shake id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> shake.name = 'First Shake'
=> "First Shake"
irb(main):003:0> shake.save
=> true
irb(main):004:0> shake.ikuras
=> []
どうもおかしいと思ってIkuraモデルを全件表示してみると、ちゃんと生成されている。
irb(main):001:0> Ikura.all
=> [#<Ikura id: 1, shake_id: 1, name: "First Ikura", created_at: "2013-08-04 01:
42:32", updated_at: "2013-08-04 01:42:32">]
Shakeクラスをリロードしてみると子モデルとして格納されている。
irb(main):002:0> shake = Shake.first
=> #<Shake id: 1, name: "First Shake", created_at: "2013-08-04 01:42:31", update
d_at: "2013-08-04 01:42:31">
irb(main):003:0> shake.ikuras
=> [#<Ikura id: 1, shake_id: 1, name: "First Ikura", created_at: "2013-08-04 01:
42:32", updated_at: "2013-08-04 01:42:32">]
単に子モデルを生成しただけではすぐにインスタンスに反映されず、下記のように明示的に親クラス側の配列に追加しておく必要があるようだ。
  def create_ikuras
    if ikuras.blank?
      ikura1 = Ikura.new
      ikura1.shake_id = id
      ikura1.name = 'First Ikura'
      ikura1.save
      ikuras << ikura1
    end
  end
ShakeモデルをNewしてsaveすると、子モデルのikurasが直ちに追加されるようなった。
irb(main):001:0> shake = Shake.new
=> #<Shake id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> shake.name = 'Second Shake'
=> "Second Shake"
irb(main):003:0> shake.save
=> true
irb(main):004:0> shake.ikuras
=> [#<Ikura id: 2, shake_id: 2, name: "First Ikura", created_at: "2013-08-04 04:
35:58", updated_at: "2013-08-04 04:35:58">]

2013年8月3日土曜日

RailsでRspecとFactory Girlをインストールするサンプル

RailsではユニットテストのためにRspec、テストデータ作成のためにFactory Girlというgemが良く使われているようなので、インストール手順をまとめておく。

まず、Rspecのインストール。
通常版とRails版の両方があるようなので、両方インストールしておく。
gem install rspec
Successfully installed rspec-2.14.1
1 gem installed
Installing ri documentation for rspec-2.14.1...
Building YARD (yri) index for rspec-2.14.1...
Installing RDoc documentation for rspec-2.14.1...
gem install rspec-rails
Successfully installed rspec-rails-2.14.0
1 gem installed
Installing ri documentation for rspec-rails-2.14.0...
Building YARD (yri) index for rspec-rails-2.14.0...
C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/yard-0.8.6.2/lib/yard/parse
r/source_parser.rb:98: warning: redundant nested repeat operator: /lib\/generato
rs\/**\/*_spec.rb/
C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/yard-0.8.6.2/lib/yard/parse
r/source_parser.rb:104: warning: redundant nested repeat operator: /lib\/generat
ors\/**\/*_spec.rb/
Installing RDoc documentation for rspec-rails-2.14.0...
Rails版の方は良くわからない警告が出ているけれどもとりあえず大丈夫そうである。

次に、Factory Girlのインストール。
こちらも通常版とRails版の両方があるようなので、両方インストールしておく。
gem install factory_girl
Successfully installed factory_girl-4.2.0
1 gem installed
Installing ri documentation for factory_girl-4.2.0...
Building YARD (yri) index for factory_girl-4.2.0...
Installing RDoc documentation for factory_girl-4.2.0...
gem install factory_girl_rails
Successfully installed factory_girl_rails-4.2.1
1 gem installed
Installing ri documentation for factory_girl_rails-4.2.1...
Building YARD (yri) index for factory_girl_rails-4.2.1...
Installing RDoc documentation for factory_girl_rails-4.2.1...
Gemfileにも、下記のように設定を追加しておく。
group :development do
  gem 'rspec'
  gem 'rspec-rails'
  gem 'factory_girl_rails'
end
そして、railsコマンドからRspecのインストール。
rails g rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
これでひとまずインストール完了。
下記のように、新規にmodelをgenerateすると、Rspec用のファイル(spec/models/XXX_spec.rb)とFactory Girl用のファイル(spec/factories/XXX.rb)が自動で生成されるようになる。
rails g model shake name:string
      invoke  active_record
      create    db/migrate/20130803121017_create_shakes.rb
      create    app/models/shake.rb
      invoke    rspec
      create      spec/models/shake_spec.rb
      invoke      factory_girl
      create        spec/factories/shakes.rb
新規に作成したmodelを'rake db:migrate'した後、下記のようにRspecを実行させると、一応動作する。
rake spec
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [33m* [0m

Pending:
 [33m  Shake add some examples to (or delete) E:/Sites/mytest/spec/models/shake_
spec.rb [0m
 [36m    # No reason given [0m
 [36m    # ./spec/models/shake_spec.rb:4 [0m

Finished in 0.027 seconds
 [33m1 example, 0 failures, 1 pending [0m

Randomized with seed 9086
デフォルトの状態ではテスト結果がpendingになっているので、まずFactory Girl用のファイル(spec/factories/shakes.rb)を編集してテストデータを準備する。
FactoryGirl.define do
  factory :shake do
    name "First Shake"
  end
end
次に、Rspec用のファイル(spec/models/shake_spec.rb)を編集する。
ここでは、ひとまず名前だけをチェックするように設定してみる。
describe Shake do
  it 'The first name of Shake must be First Shake.' do
    shake = FactoryGirl.create(:shake)
    expect(shake.name).to eq('First Shake')
  end
end
ところが、実行するとエラーが出てしまう。
rake spec
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [31mF [0m

Failures:

  1) Shake The first name of Shake must be First Shake.
      [31mFailure/Error: [0m  [31mshake = FactoryGirl.create(:shake) [0m
      [31mNameError [0m:
        [31muninitialized constant FactoryGirl [0m
 [36m     # ./spec/models/shake_spec.rb:5:in `block (2 levels) in <top (required
)>' [0m

Finished in 0.032 seconds
 [31m1 example, 1 failure [0m

Failed examples:

 [31mrspec ./spec/models/shake_spec.rb:4 [0m  [36m# Shake The first name of Shak
e must be First Shake. [0m

Randomized with seed 14976
Rspecの設定ファイル(spec/spec_helper.rb)に、Rails版のFactory Girlの設定を下記のように追加しておく必要があるようだ。
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

require 'factory_girl_rails'
設定後に再度Rspecを実行すると、無事に成功した。
rake spec
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [32m. [0m

Finished in 0.09301 seconds
 [32m1 example, 0 failures [0m

Randomized with seed 4852

2013年5月11日土曜日

JavaScriptでポップアップウィンドウを表示する最小サンプル

ポップアップウィンドウを最も簡単に実現できるのは、JavaScriptのalert関数である。

alertのサンプル

しかしながら、alert関数では表示するテキストをパラメータとして渡せるだけなので、ポップアップウィンドウとは言い難い。

次に簡単に実現できるのは、window.openを利用する方法である。

window.openのサンプル

用途によってはこれでも十分なのだが、要はブラウザの新規ウィンドウを開いているだけなので、ウィンドウの細かい制御はできないし、セキュリティの設定によってはポップアップウィンドウの警告が表示されてしまう場合もある。

現実的な方法としては、jQueryとCSSを使う方法になる。

まず、ページ内に、下記のようにポップウィンドウとして表示するDIVタグを用意しておく。
<div id="popupwindow">
  <div id="popuowindow_message"></div>
  <div id="popupwidow_close">OK</div>
</div>
このままでは、DIVタグの中身が丸見えなので、スタイルシートで下記のように非表示にしておく。
#popupwindow {
  display: none;
}
JavaScript側では、下記のように記述する。
$("#popupwindow3").click(function(e){
  $('#popuowindow_message').text('こんにちは!');
  $('#popupwindow').css({top: 400, left: 20}).fadeIn(100);
  $('#popupwidow_close').click(function() {$('#popupwindow').fadeOut(100);});
  return false;
});
1. メッセージのテキストを設定。
2. ポップアップウィンドウの表示。
3. OKがクリックされた場合にウィンドウを非表示。

popup windowで表示するサンプル

最小サンプルなので、余計な装飾などは行っていないが、DIVタグをCSSで装飾して見栄えを良くしたり、JavaScriptのイベントを追加してタイトルバーでドラッグできるようにするなど、いろいろなことはできるようだ。

今回作成したソースコードの参照はこちらから。

https://gist.github.com/torafugu/5559183

今回参考にさせていただいたブログ記事。

jQueryで「window.open」タイプのポップアップウィンドウ方法
http://black-flag.net/jquery/20100630-1210.html

ドラッグ可能なポップアップウインドウを作ろう - jQuery入門
http://ponk.jp/jquery/basic/ipop

2013年5月8日水曜日

Formで指定したmodel以外のmodelを指定してコンボボックスを生成するサンプル


上図のようなmodelの構造で、DouroFusetsuの更新画面を考えてみる。

まずはscaffold。(実行結果は省略。)
rails generate scaffold douro_fusetsu shichoson:references douro:references
デフォルトの状態の画面では、下図のように型をreferencesにして作成したmodelクラスのハッシュを入力しなければならないので、


コンボボックスでIDを選択できるように、下図のように変更する。


View側のソースは下記のようになる。
<%= form_for(@douro_fusetsu) do |f| %>
  <% if @douro_fusetsu.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@douro_fusetsu.errors.count, "error") %> prohibited this douro_fusetsu from being saved:</h2>

      <ul>
      <% @douro_fusetsu.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :shichoson %><br />
    <%= f.collection_select(:shichoson_id, Shichoson.find(:all), :id, :name) %>
  </div>
  <div class="field">
    <%= f.label :douro %><br />
    <%= f.collection_select(:douro_id, Douro.find(:all), :id, :name) %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
市町村では、選択肢の数が多すぎる(日本全国で1742もある)ので、都道府県を選択して数を絞り込めるようにしたい。

TodofukenとShichosonは親子関係であるものの、DouroFusetsuはTodofukenと直接関係がないので、f.collection_selectは使うことができない。

その代わりに、下記のように、f.をつけずにcollection_selectを使うことにする。
<%= collection_select(:douro_fusetsu, "todofuken_id", Todofuken.find(:all), :id, :name) %>
しかしながらエラーが表示されてしまう。


f.がついていなくても、formの名前が同じ場合は、form_forで関連づけられているmodelのメンバーではないということでエラーになってしまうようだ。

下記のように、シンボル:douro_fusetsu ではなく、まったく別の名前を指定すると、
<%= collection_select("help", "todofuken_id", Todofuken.find(:all), :id, :name) %>
エラーが出ずに表示されるようになった。


2013年5月6日月曜日

Railsで親テーブルの情報を指定してfindするサンプル


上図のような親子関係の場合に、親テーブル(Todoufuken)から子テーブル(Shichoson)をincludeして、子テーブルの情報を指定してfindするというサンプルは既にあるようだ。

[memo][Rails]連携先テーブルの条件を指定してfind
http://bugcloud.com/?p=571

実際に、下記のように記述すると実現できる。
Todofuken.all(:include => :shichosons, :conditions => ["shichosons.name = ?", '名古屋市'])
ところが、同様の記述で、子テーブル(Shichoson)から親テーブル(Todoufuken)をincludeして、親テーブルの情報を指定してfindしようとしても、うまくいかない。
Shichoson.all(:include => :todofuken, :conditions => ["todofuken.name = ?", '愛知県'])
ちなみに、findのincludeではなく、下記のように子テーブルを特定した上でリンクした記述では動作している。
Shichoson.first.todofuken.name
conditionsの部分のテーブル名の指定は、model側で単数形でbelongs_toとなっている場合でも、
class Shichoson < ActiveRecord::Base
  belongs_to :todofuken
end
このように複数形を指定する必要があるようだ。
Shichoson.all(:include => :todofuken, :conditions => ["todofukens.name = ?", '愛知県'])
シンボル(:xxxx)として指定した場合と、実際のテーブル名("xxxxs")として指定した場合の違いということなのだろうが、統一感がなくてややこしい。。

今回は、下記の記事を参考にさせていただいた。

rails:3610
http://www.fdiary.net/ml/rails/msg/3610

2013年2月21日木曜日

Railsで表形式のデータを複数同時に更新するサンプル(新規追加対応版)


前回のサンプルで、更新に加えて削除もできるように改修を行った。
今回は、同じ画面から新規追加も行えるようにしてみたい。

scaffoldの内容は前回からクラスの名前を変えただけ。
rails g scaffold imp2_ramen name:string price:integer
rake db:migrate
新規追加のために、add_rowというルートを新たに追加。それ以外は前回から変更なし。
  match '/imp2_ramen/add_row' => 'imp2_ramen#add_row'
  match '/imp2_ramen/update_all' => 'imp2_ramen#update_all'
  match '/imp2_ramen/confirm_all' => 'imp2_ramen#confirm_all'
  match '/imp2_ramen/edit_all' => 'imp2_ramen#edit_all', as: 'edit_all_imp2_raman', :via => :get
  resources :imp2_ramen
controllerの確認画面(confirm_all)の箇所を下記のように編集。
パラメーターとしてIDが飛んで来なければ、新規追加と判断してクラスをnewしている。
  # GET /imp2_ramen/confirm_all
  def confirm_all
    @imp2_ramen = Array.new
    for imp2_raman in params[:imp2_ramen]
      unless imp2_raman["id"].blank?
        updated_imp2_raman = Imp2Raman.find(imp2_raman["id"])
        updated_imp2_raman.update_attributes(imp2_raman)
        @imp2_ramen << updated_imp2_raman
      else
        @imp2_ramen << Imp2Raman.new(imp2_raman)
      end
    end
    session[:imp2_ramen] = @imp2_ramen
  end
今回追加したadd_rowというルートのcontroller側の記述は下記の通り。
実は何もしていない(笑)。

クライアント側のJavaScriptだけで完結することも可能なのだが、Railsの<%~%>タグが使いたいので、一度サーバに飛ばすようにしている。
  # GET /imp2_ramen/add_row
  def add_row
    respond_to do |format|
      format.js { render :layout => false }
    end
  end
複数同時更新画面(edit_all)のviewは下記の通り。

「行追加」のボタンを追加したことと、新規の場合はチェックボックスの代わりに「新規」という文字を表示するようにしたことが変更点。
<h1>Editing all imp2_ramen</h1>

<%= form_tag :action => 'confirm_all' do %>

<table border="1">
  <tr>
    <th>新規/削除</th>
    <th>Name</th>
    <th>Price</th>
  </tr>
  <% for imp2_raman in @imp2_ramen %>
    <tr>
      <td>
        <% unless imp2_raman.id.blank? %>
          <%= hidden_field_tag("imp2_ramen[][id]", imp2_raman.id) %>
          <%= check_box_tag("imp2_ramen[][delete_check]", true, imp2_raman.delete_check) %>
        <% else %>
          <%= '新規' %>
        <% end %>
      </td>
      <td>
        <%= text_field_tag("imp2_ramen[][name]", imp2_raman.name) %>
      </td>
      <td>
        <%= number_field_tag("imp2_ramen[][price]", imp2_raman.price) %>
      </td>
    </tr>
  <% end %>
</table>
<%= submit_tag "入力確認" %>
<input id="add_row_button" type="button" value="行追加" />
<% end %>

<%= link_to 'Back', imp2_ramen_path %>
「行追加」のボタンがクリックされると、app/assets/javascript/imp2_ramen.js.coffeeで、下記の処理を実行。
クリックを検知して、ajaxとしてadd_row.jsのpathを読みに行くだけ。
jQuery ->
  jQuery('#add_row_button').click ->
    $.get("add_row.js")
add_row.jsは下記の内容となる。
tableの最後に空行を追加している。
$('tbody').append('<tr><td>新規</td><td><%= text_field_tag("imp2_ramen[][name]") %></td><td><%= number_field_tag("imp2_ramen[][price]") %></td></tr>')
このファイルではなく、前述のimp2_ramen.js.coffee内でinputタグを直接記述すれば同じことができるはずなのだが、Railsの場合はinputタグ内のidの付け方が独特なので、保守性を考慮してview側と同様の記述(<%~%>)を行うようにしている。

確認画面(confirm_all)のviewは下記の通り。
<h1>Editing all imp2_ramen</h1>

<table border="1">
  <thead>
    <tr>
      <th>新規/削除</th>
      <th>Name</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
  <% for imp2_raman in @imp2_ramen %>
    <tr>
      <td>
        <% if imp2_raman.delete_check %>
          <%= '削除' %>
        <% elsif imp2_raman.id.blank? %>
          <%= '新規' %>
        <% end %>
      </td>
      <td>
        <%= imp2_raman.name %>
      </td>
      <td>
        <%= imp2_raman.price %>
      </td>
    </tr>
  <% end %>
  </tbody>
</table>

<%= form_tag :action => 'update_all' do %>
<%= submit_tag "更新" %>
<% end %>

<%= link_to 'Back', edit_all_imp2_raman_path %>
IDが存在しない場合に、「新規」と表示している以外は前回から変更なし。
controller側で新規追加分も含めてImp2Ramanクラスの配列としてセッションに格納しているので、view側では同じ配列をループさせて値を取り出すのみ。

実際の画面遷移の例は以下のようになる。

一覧画面(index)

更新画面(edit_all)

更新画面(edit_all)で新規追加。

確認画面(confirm_all)

更新後の一覧画面(index)


今回作成したソースコードの参照はこちらから。

https://github.com/torafugu/mytest/blob/master/app/assets/javascripts/imp2_ramen.js.coffee
https://github.com/torafugu/mytest/tree/master/app/views/imp2_ramen
https://github.com/torafugu/mytest/blob/master/app/controllers/imp2_ramen_controller.rb

実際に動くサンプルはこちらから。(heroku上のサイト)

2013年2月19日火曜日

Railsで表形式のデータを複数同時に更新するサンプル(削除対応版)


前回作成したサンプルで、複数件の更新はひとまず対応できた。
今回は、選択した行のデータを削除できる機能を追加してみたい。

まずはscaffold。クラスの名前を変えただけで、フィールドは同じ。(nameとprice)
rails g scaffold imp_ramen name:string price:integer
rake db:migrate
次にroutes.rbの編集。これも前回と同様。画面遷移も同じになる。
  match '/imp_ramen/update_all' => 'imp_ramen#update_all'
  match '/imp_ramen/confirm_all' => 'imp_ramen#confirm_all'
  match '/imp_ramen/edit_all' => 'imp_ramen#edit_all', as: 'edit_all_imp_raman', :via => :get
  resources :imp_ramen
modelに新たにdelete_checkというフィールドを追加する。
class ImpRaman < ActiveRecord::Base
  attr_accessor :delete_check
end
次にcontroller。index、edit_all、confirm_allは前回から変更なし。
update_allを下記のように編集。
  # GET /imp_ramen/update_all
  def update_all
    @imp_ramen = session[:imp_ramen]
    for imp_raman in @imp_ramen
      if imp_raman.delete_check
        to_destory_raman = ImpRaman.find(imp_raman.id)
        to_destory_raman.destroy
      else
        imp_raman.save
      end
    end
    session[:imp_ramen] = nil
    respond_to do |format|
      format.html { redirect_to imp_ramen_url }
      format.json { head :no_content }
    end
  end
delete_checkがtrueの場合に、該当行のデータのdestroyを実行。

to_destory_ramanに代入し直しているのは、imp_ramanはループ中の配列の要素なので、ここでdestoryするとエラーが発生してしまうため。

DBに反映後は、セッションはもう不要なので、nilを代入してクリア。

indexのviewを指定してレンダリングすると変更が反映されない中途半端な状態で表示されてしまうので、indexのURLへリダイレクトしている。

複数同時更新画面(edit_all)のviewは下記の通り。
<h1>Editing all imp_ramen</h1>

<%= form_tag :action => 'confirm_all' do %>

<table border="1">
  <tr>
    <th>削除</th>
    <th>Name</th>
    <th>Price</th>
  </tr>
  <% for imp_raman in @imp_ramen %>
    <tr>
      <td>
        <%= hidden_field_tag("imp_ramen[][id]", imp_raman.id) %>
        <%= check_box_tag("imp_ramen[][delete_check]", true, imp_raman.delete_check) %>
      </td>
      <td>
        <%= text_field_tag("imp_ramen[][name]", imp_raman.name) %>
      </td>
      <td>
        <%= number_field_tag("imp_ramen[][price]", imp_raman.price) %>
      </td>
    </tr>
  <% end %>
</table>
<%= submit_tag "入力確認" %>
<% end %>

<%= link_to 'Back', imp_ramen_path %>
check_boxはチェックされている場合のみ値が送信されるので、チェックされていない行があると位置がズレてしまう。
hidden_fieldをcheck_boxの前に持ってくると、位置ズレを防ぐことができる。

check_box_tagでは、3番目の引数(チェックされた場合に送信される値)で明示的にtrueをしておかないと、デフォルトでは"on"という妙な値が送信されるので、controller側で編集のために余計なコードが必要になる。

確認画面(confirm_all)のviewは下記の通り。
<h1>Editing all imp_ramen</h1>

<table border="1">
  <tr>
    <th>削除</th>
    <th>Name</th>
    <th>Price</th>
  </tr>
  <% for imp_raman in @imp_ramen %>
    <tr>
      <td>
        <% if imp_raman.delete_check %>
          <%= '削除' %>
        <% end %>
      </td>
      <td>
        <%= imp_raman.name %>
      </td>
      <td>
        <%= imp_raman.price %>
      </td>
    </tr>
  <% end %>
</table>

<%= form_tag :action => 'update_all' do %>
<%= submit_tag "更新" %>
<% end %>

<%= link_to 'Back', edit_all_imp_raman_path %>
delete_checkの値をチェックして、trueであれば「削除」を表示している。

実際の画面遷移の例は以下のようになる。

一覧画面(index)

更新画面(edit_all)

確認画面(confirm_all)

更新後の一覧画面(index)


今回作成したソースコードの参照はこちらから。

https://github.com/torafugu/mytest/tree/master/app/views/imp_ramen
https://github.com/torafugu/mytest/blob/master/app/models/imp_raman.rb
https://github.com/torafugu/mytest/blob/master/app/controllers/imp_ramen_controller.rb

実際に動くサンプルはこちらから。(heroku上のサイト)


今回参考にさせていただいたブログの記事。


2013年2月18日月曜日

Railsで表形式のデータを複数同時に更新するサンプル


Railsのscaffoldで作成されるedit画面は、1件のデータしか更新できない。
データの数が多い場合は、1件1件edit画面を開いて更新するのは面倒である。
一覧形式で複数件同時に更新できる画面を作成してみる。

まずはscaffold。
rails g scaffold ramen name:string price:integer
rake db:migrate
ラーメンでramenというクラス名にしてみたのだが、menがmanの複数形として判断されるので、クラスの単数形はramanとなってしまった。

まぁこれはこれで識別しやすいのでこのまま利用する。

次にroutes.rbの編集。以下を追加した。
  match '/ramen/update_all' => 'ramen#update_all'
  match '/ramen/confirm_all' => 'ramen#confirm_all'
  match '/ramen/edit_all' => 'ramen#edit_all', as: 'edit_all_raman', :via => :get
  resources :ramen
画面遷移としては、


という流れになる。

次にcontrollerを編集する。
  # GET /ramen
  # GET /ramen.json
  def index
    @ramen = Raman.all
    session[:ramen] = @ramen

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @ramen }
    end
  end
まず一覧画面(index)の検索結果をセッション(session[:ramen])に入れておく。
複数同時更新画面(edit_all)では、DBからではなく、セッションから取得する。
  # GET /ramen/edit_all
  def edit_all
    @ramen = session[:ramen]
  end
セッションを使う理由は、確認画面(confirm_all)から更新画面(edit_all)に戻った場合に変更部分を維持するため。
確認画面(confirm_all)では、画面から送られたパラメータの値でRamanクラスを編集して再度セッションに格納している。
  # GET /ramen/confirm_all
  def confirm_all
    @ramen = Array.new
    for raman in params[:ramen]
      updated_raman = Raman.find(raman["id"])
      updated_raman.update_attributes(raman)
      @ramen << updated_raman
    end
    session[:ramen] = @ramen
  end
@ramenはRamanクラスの配列で、モデルで定義している型と同じではないために、パラメータの値をそのままクラスとして利用することができずに、ちょっと面倒なことをしている。

更新処理(update_all)では、セッションから値を取得してDBにsaveするのみ。
  # GET /ramen/update_all
  def update_all
    @ramen = session[:ramen]
    for raman in @ramen
      raman.save
    end
    render action: "index"
  end
複数同時更新画面(edit_all)のviewは下記の通り。
<h1>Editing all ramen</h1>

<%= form_tag :action => 'confirm_all' do %>

<table border="1">
  <tr>
    <th>ID</th>
    <th>Name</th>
    <th>Price</th>
  </tr>
  <% for raman in @ramen %>
    <tr>
      <td>
        <%= raman.id %>
        <%= hidden_field_tag("ramen[][id]", raman.id) %>
      </td>
      <td>
        <%= text_field_tag("ramen[][name]", raman.name) %>
      </td>
      <td>
        <%= number_field_tag("ramen[][price]", raman.price) %>
      </td>
    </tr>
  <% end %>
</table>
<%= submit_tag "入力確認" %>
<% end %>

<%= link_to 'Back', ramen_path %>
@ramenがRamanクラスの配列なので、ループを回して一つづつ取り出して、inputタグを作成している。
"ramen[][XXXX]"と記述すると、ramenという名前のパラメータの配列の項目名として扱ってくれるようだ。

確認画面(confirm_all)のviewは下記の通り。
<h1>Editing all ramen</h1>

<table border="1">
  <tr>
    <th>ID</th>
    <th>Name</th>
    <th>Price</th>
  </tr>
  <% for raman in @ramen %>
    <tr>
      <td>
        <%= raman.id %>
      </td>
      <td>
        <%= raman.name %>
      </td>
      <td>
        <%= raman.price %>
      </td>
    </tr>
  <% end %>
</table>

<%= form_tag :action => 'update_all' do %>
<%= submit_tag "更新" %>
<% end %>

<%= link_to 'Back', edit_all_raman_path %>
controllerが既にセッションに値を確認しているので、ここでは単純に表示するだけ。

実際の画面遷移の例は以下のようになる。

一覧画面(index)

更新画面(edit_all)
確認画面(confirm_all)
更新後の一覧画面(index)
今回作成したソースコードの参照はこちらから。

https://github.com/torafugu/mytest/tree/master/app/views/ramen
https://github.com/torafugu/mytest/blob/master/app/models/raman.rb
https://github.com/torafugu/mytest/blob/master/app/controllers/ramen_controller.rb

実際に動くサンプルはこちらから。(heroku上のサイト)

http://ancient-savannah-6605.herokuapp.com/ramen

今回参考にさせていただいたブログの記事。

Rails のフォームで配列形式のデータを扱う方法
http://webos-goodies.jp/archives/51387214.html

Ruby on Railsあれこれ/複数従業員情報の一括更新
http://winter-tail.sakura.ne.jp/pukiwiki/index.php?Ruby%20on%20Rails%A4%A2%A4%EC%A4%B3%A4%EC%2F%CA%A3%BF%F4%BD%BE%B6%C8%B0%F7%BE%F0%CA%F3%A4%CE%B0%EC%B3%E7%B9%B9%BF%B7

2013年2月16日土曜日

RailsでIDだけのテーブルを作ってみる

Railsでシーケンス番号を取得するためのテーブルを使いたくなったので、ID以外に列のないmodelを作成してみた。

まずはmodelのgenerate。
G:\Sites\mytest>rails generate model idonly
      invoke  active_record
      create    db/migrate/20130214223210_create_idonlies.rb
      create    app/models/idonly.rb
      invoke    test_unit
      create      test/unit/idonly_test.rb
      create      test/fixtures/idonlies.yml
次にdbのmigration。
G:\Sites\mytest>rake db:migrate
==  CreateRamen: migrating ====================================================
-- create_table(:ramen)
   -> 0.1920s
==  CreateRamen: migrated (0.1940s) ===========================================

==  CreateIdonlies: migrating =================================================
-- create_table(:idonlies)
   -> 0.0070s
==  CreateIdonlies: migrated (0.0100s) ========================================
特に問題なく作成できた。
G:\Sites\mytest>rails c
Loading development environment (Rails 3.2.1)
irb(main):001:0> id = Idonly.new
=> #<Idonly id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> id.save
=> true
irb(main):003:0> Idonly.all
=> [#<Idonly id: 1, created_at: "2013-02-14 22:37:25", updated_at: "2013-02-14 2
2:37:25">]
modelのnewもできるし検索もできる。
IDだけのmodelでも普通に使えるようである。

2013年1月30日水曜日

RailsのirbでSQLを表示しない方法

Railsのirbはとても便利なのだが、デフォルトの状態では余計なSQLが表示されてしまうので少々見づらい。


下記のように、ログを出力しないコマンドをirb上で入力すると、
ActiveRecord::Base.logger = nil
SQLが表示されなくなった。


irbを起動するたびに毎回入力するのは面倒なので、設定ファイルにあらかじめ書いておきたい。
設定ファイルは、~/.irbrc らしいのだが、Windowsではどこに該当するのかわからない。

Windows上のRubyでは、"~/"の場所は環境変数"HOME"で指定されているようだ。
下記のコマンドを入力すると、環境変数HOMEの内容が表示される。
ruby -e 'print ENV["HOME"]'
私の場合は、C:\Users\kaneda だったので、C:\Users\kaneda\.irbrc (.irbcではないことに注意)を作成して、上記のログのコマンドを書いておくと、irb起動直後からSQLが表示されないようになった。


以下は、今回参考にさせていただいたホームページ。

How can you hide database output in Rails console?
http://stackoverflow.com/questions/7724188/how-can-you-hide-database-output-in-rails-console

irbを便利につかうために for windows
http://d.hatena.ne.jp/japanrock_pg/20080716/1216191195

2013年1月23日水曜日

Processing.jsでSVGのグラデーションは表示できない

<linearGradient id="grad1" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ff6347"/>
<stop offset="100%" stop-color="#f5deb3"/>
</linearGradient>
<path d="M40,10 l30,30 l-15,0 l0,60 l-30,0 l0,-60 l-15,0 l30,-30z" stroke="#ff6347" stroke-width="2" fill="url(#grad1)" />
上記のコードで、下図のようにSVGでグラデーションを表示できる。


ところが、Processing.js で上記のSVGを表示すると、グラデーションの部分が黒くなってしまう。


SVGをpngに変換して表示すると、グラデーションがきちんと現れる。


どうやら、Processing.jsでは、SVGのグラデーションは非対応のようである。

2012年12月4日火曜日

Processing.jsでSVG画像をその場で回転させるサンプル

ProcessingでSVG画像を読み込んで回転させようとすると、その場で回転せずに、下図のように画像の左上の頂点を中心にして回転してしまう。


同じ場所で回転させるためには、(1)の移動の際には、右方向に1マス分、(2)の移動の際には、右方向と下方向に1マス分という具合に、回転した後で画像自体を動かす必要があるようだ。

Processing の関数内で実装すると下記のようになる。
void moveRect2() {
  int xAdjust = 0;
  int yAdjust = 0;
  background(200);
  fill(200);
  myicon.resetMatrix();
  if (currentAngle == TWO_PI) {
    currentAngle = 0;
  } else {
    currentAngle += HALF_PI;
  }
  myicon.rotate(currentAngle);
  if (currentAngle == HALF_PI) {
    xAdjust = 30;
    yAdjust = 0;
  } else if (currentAngle == PI) {
    xAdjust = 30;
    yAdjust = 30;
  } else if (currentAngle == (PI + HALF_PI)) {
    xAdjust = 0;
    yAdjust = 30;
  } else {
    xAdjust = 0;
    yAdjust = 0;
  }
  shape(myicon, 50 + xAdjust, 50 + yAdjust, 30, 30);
}

実際に動作するサンプルはこちら。
"Rotate1"ボタンを押すと通常の回転、"Rotate2"ボタンを押すとその場から動かずに回転する。


ソースコード全文はこちら。
https://gist.github.com/4204624

2012年11月19日月曜日

画面オープン直後にProcessing.jsのスクリプトを実行するサンプル

jQueryで画面のオープン直後に処理を実行する場合は、$(document).ready を利用することが一般的なようだが、このイベントに合わせてProcessingのスクリプトを実行しようとしてもうまくいかない。

具体的には、下記のようなソースコードでは、
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="jquery.js"></script>
<script src="processing-1.4.1.js" type="text/javascript"></script>
<script type="text/processing" data-processing-target="mycanvas">
void setup()
{
  size(200, 200);
  colorMode(RGB, 100);
  noLoop();
}

void draw()
{
  fill(90);
  ellipse(70, 70, 100, 100);
}

void addEllipse()
{
  fill(60);
  ellipse(90, 90, 70, 70);
}
</script>
<script type="text/javascript">
$(document).ready(function(){
  var p= Processing.getInstanceById("mycanvas");
  p.addEllipse();
})
</script>
</head>
<body>
  <p>
  <canvas id="mycanvas"></canvas>
  </p>
  </body>
</html>
次のようなエラーメッセ―ジが表示されてしまう。


$(document).ready イベントでは、画面上のオブジェクトの読み込みがすべて完了した時点ではなく、画面上のDOM(Document Object Model)の読み込みが完了した時点、つまり、HTMLのソースコードの読み込みが完了した時点となる。

このタイミングでは、Processingの画面描画が完了していないので、Processingの関数を呼ぼうとしても、エラーになってしまうようだ。

$(document).ready の代わりに、$(window).load を使ったらうまくいった。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="jquery.js"></script>
<script src="processing-1.4.1.js" type="text/javascript"></script>
<script type="text/processing" data-processing-target="mycanvas">
void setup()
{
  size(200, 200);
  colorMode(RGB, 100);
  noLoop();
}

void draw()
{
  fill(90);
  ellipse(70, 70, 100, 100);
}

void addEllipse()
{
  fill(60);
  ellipse(90, 90, 70, 70);
}
</script>
<script type="text/javascript">
$(window).load(function(){
  var p= Processing.getInstanceById("mycanvas");
  p.addEllipse();
})
</script>
</head>
<body>
  <p>
  <canvas id="mycanvas"></canvas>
  </p>
  </body>
</html>

2012年11月12日月曜日

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル(インタフェース改良版)


前回作成したサンプルで、一応リストボックスとしては機能しているものの、登録件数が多くなると何が選択されていて、何が選択されていないかがわかりにくくなる。

下記のようなインタフェースで、左側のリストボックスが未登録、右側のリストボックスが登録済となるように分割して表示したい。


まずは再改良版のGameクラスとLineupクラスのscaffold。(実行結果は省略。)
rails g scaffold bestgame date:date
rails g scaffold bestlineup bestgame:references player:references
rake db:migrate
Gameクラスの_form.html.erbは、下記のように修正する。
<%= form_for(@bestgame) do |f| %>
  <% if @bestgame.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@bestgame.errors.count, "error") %> prohibited this bestgame from being saved:</h2>

      <ul>
      <% @bestgame.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true %>
  </div>
  <div class="field">
    <table>
      <tr><td align="center">
        <%= f.label "Bench Lineups" %><br />
        <%= f.select(:non_lineup_ids, @non_lineup_select_data, {}, {:multiple => true, :size => Player.all.size}) %>
      </td><td>
        <input id="btnMoveRight" type="button" value="->" onClick="moveItems('right')" />  
        <br />  
        <input id="btnMoveLeft" type="button" value="<-" onClick="moveItems('left')" />
        </td><td align="center">
        <%= f.label "Starting Lineups" %><br />
        <%= f.select(:player_ids, @lineup_select_data, {}, {:multiple => true, :size => Player.all.size}) %>
      </td></tr>
      </table>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
左側のリストボックスは、Lineupクラスに登録されていないPlayerを表示する。>
右側のリストボックスは、Lineupクラスに登録されているPlayerを表示する。
右側のリストボックスの送信データのみ、model側で扱うようにする。

左右のリストボックスのデータの移動は、矢印のボタンを押すと、下記のJavaScriptの関数が呼ばれるようにする。
// リストボックスの項目選択
function moveItems(direction) {
  
  var leftBox = document.getElementById("bestgame_non_lineup_ids");  
  var rightBox = document.getElementById("bestgame_player_ids");   
  var fromBox, toBox;  

  if (direction == "right") {  
    fromBox = leftBox; toBox = rightBox;  
  }   
  else if (direction == "left") {  
    fromBox = rightBox; toBox = leftBox;  
  }  
    
  if ((fromBox != null) && (toBox != null)) {   
    if(fromBox.length < 1) {  
      alert("リストボックスにアイテムがありません!");  
      return false;  
    }  
    if(fromBox.selectedIndex == -1) {  
      alert("移動するアイテムを選択してください!");  
      return false;  
    }  
    while ( fromBox.selectedIndex >= 0 ) {   
      var newOption = new Option();   
      newOption.text = fromBox.options[fromBox.selectedIndex].text;   
      newOption.value = fromBox.options[fromBox.selectedIndex].value;   
      toBox.options[toBox.length] = newOption;  
      fromBox.remove(fromBox.selectedIndex);   
    }   
  }  
  return false;
}

// Submit時のリストボックスの項目自動選択
jQuery(function(){
  $('[id*=bestgame]').submit(function(){
    
    var selectedIDs = new Array();
    $('#bestgame_player_ids option:not(:selected)').each(function() {  
      selectedIDs.push(this.value)
    });
    $('#bestgame_player_ids').val(selectedIDs)
  });
});
moveItemsの関数では、選択されたリストボックスの値と表示テキストを移動先に追加、移動元から削除している。データがない場合とデータ未選択の場合のエラーチェックも行なっている。

$('[id*=bestgame]').submitの関数では、フォームのsubmit時に、右側のリストボックスの値を自動的に全選択するようにしている。

右側のリストボックスにデータを移しただけでは、フォームに値が送信されない。リストボックスは、あくまで選択された値だけがフォームに送信されるからである。

要素名を部分一致にしているのは、railsの場合、newのフォームとeditのフォームで、名前が異なるため。

modeには、下記のメソッドを追加する。
  # Lineupクラスに存在しないPlayerのIDを配列でget/setできる属性を追加
  def non_lineup_ids
    @non_lineup_ids || players.collect{|p| p.id} - bestlineups.collect{|p| p.id}
  end
 
  def non_lineup_ids=(id_array)
    @non_lineup_ids = id_array.collect{|id| id.to_i};
  end
Lineupに存在しないプレイヤーを、playersとlineupsの配列の引き算で算出している。ただし、Controllerからは使用していない。

最後に、Controllerの修正は下記の通りとなる。(new~update部分のみ)
  # GET /bestgames/new
  # GET /bestgames/new.json
  def new
    @bestgame = Bestgame.new
    @non_lineup_select_data = Player.all.collect{|p| [p.name, p.id]}
    @lineup_select_data = ""

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @bestgame }
    end
  end

  # GET /bestgames/1/edit
  def edit
    @bestgame = Bestgame.find(params[:id])
    @non_lineup_select_data = Player.all.collect{|p| [p.name, p.id]} - Bestlineup.find_all_by_bestgame_id(params[:id]).collect{|l| [l.player.name, l.player.id]}
    @lineup_select_data = Bestlineup.find_all_by_bestgame_id(params[:id]).collect{|l| [l.player.name, l.player.id]}
  end
新規追加の場合は、登録済のデータが存在しないので、lineup_select_dataの配列を空で初期化している。
修正の場合は、未登録のデータを、playersとlineupsの配列を引き算することで導き出している。modelクラスの関数を使わずに、ここで計算しているのは、modelクラスの関数はidのみ扱っていて、名前を保持していないため。

これで、2つのリストボックスでデータを移動できるインタフェースが完成した。


今回は下記のホームページが参考になった。

リストボックス間でアイテムを移動
http://jsajax.com/Articles/listbox/339

2012年11月8日木曜日

RubyでExcelを操作するサンプル

FXのシステムトレードのブログを運用している。

不労所得を確保セヨ!!シストレ24戦記
http://sysken.blogspot.jp/

FX会社から提供される取引報告のExcelファイルから、HTMLの表組みを手作業で行なっていたのだが、件数が多いと時間がかかる。

そこで、下記の記事を参考にして、RubyでExcelファイルから値を抜き出してHTMLに変換するプログラムを作成してみた。

VBA より便利で手軽 Excel 操作スクリプト言語「Ruby」へのお誘い (前編)
http://jp.rubyist.net/magazine/?0027-ExcellentRuby

Excelで値を取り出してしまえば、後は通常のRubyのプログラムと同様に値を扱えば良い。

数字をカンマ区切りにするのは、下記の記事を参考にさせていただいた。

Re: 金額カンマ編集について
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37580

以下、作成したソースコード。
# encoding: CP932
require 'win32ole'

# ストラテジー別の最終取り引き回数(手動で変更)
RIKIWIKITAVYFX_USDJPY_LAST = 173
PMINVESTCAPITAL_EURGBP_LAST = 95
DBSWING_EURUSD_LAST = 28
T_Follow_EURUSD_LAST = 10

# 表組み用HTML
TABLE_HEADER = '<table border="1" bordercolor="#888" cellspacing="0" style="border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px;"><tbody>' + "\n"
TABLE_FOOTER = '</tbody></table>' + "\n"
FIRST_TAG = '<tr><td style="width: 150px;">'
SECOND_TAG = '</td><td style="text-align: right; width: 100px;"><b><span style="color: blue;">'
THIRD_TAG = '</span></b></td><td style="text-align: right; width: 100px;"><b><span style="color: blue;">'
FOURTH_TAG = '</span></b></td></tr>' + "\n"

# ExcelファイルのOpen
app = WIN32OLE.new('Excel.Application')
book = app.Workbooks.Open(app.GetOpenFilename)

# Excelファイルからのデータ抽出
rowIndex = 0
rikiwikitavyfxUSDJPYrowArray = Array.new
pminvestcapitalEURGBProwArray = Array.new
dbswingEURUSDrowArray = Array.new
tfollowEURUSDrowArray = Array.new

for row in book.ActiveSheet.UsedRange.Rows do
  if rowIndex > 0
    colIndex = 0
    colText = ""
    colArray = Array.new
    colArray << ''
    for cell in row.Columns do
      if colIndex == 2 || colIndex == 4 || colIndex.between?(10,12)
        colArray << cell.Value
      end
      colIndex += 1
    end
    if colArray[1] == "RikiTikiTavi FX" && colArray[2] == "USDJPY"
      rikiwikitavyfxUSDJPYrowArray << colArray 
    elsif colArray[1] == "Pminvestcapital" && colArray[2] == "EURGBP"
      pminvestcapitalEURGBProwArray << colArray 
    elsif colArray[1] == "DBSwing" && colArray[2] == "EURUSD"
      dbswingEURUSDrowArray << colArray 
    elsif colArray[1] == "T Follow" && colArray[2] == "EURUSD"
      tfollowEURUSDrowArray << colArray 
    end
  end
  rowIndex += 1 
end

# HTML表組み用関数
def createTable(rowArray, serial, labelSerial)
  
  sumPips = 0
  sumMoney = 0

  table = "<b>" + labelSerial.to_s + ". "
  table += rowArray[0][1] + ":"
  table += rowArray[0][2].slice(0, 3) + "/" + rowArray[0][2].slice(3, 3)
  table += "</b><br />\n"
  table += "<br />\n"
  table += "<br />\n"
  table += "<br />\n"
  table += TABLE_HEADER
  rowArray.reverse!
  for row in rowArray do
    table += FIRST_TAG
    table += serial.to_s + "回目の取り引き"
    if row[4] < 0
      table += SECOND_TAG.sub("blue", "red")
    else
      table += SECOND_TAG
    end
    table += row[4].to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "pips"
    if row[5] < 0
      table += THIRD_TAG.sub("blue", "red")
    else
      table += THIRD_TAG
    end
    table += row[5].round.to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "円"
    table += FOURTH_TAG
    serial += 1
    sumPips += row[4] * 10
    sumMoney += row[5]
  end
  table += FIRST_TAG
  table += "合 計"
  if sumPips < 0
    table += SECOND_TAG.sub("blue", "red")
  else
    table += SECOND_TAG
  end
  table += (sumPips / 10).to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "pips"
  if sumMoney < 0
    table += THIRD_TAG.sub("blue", "red")
  else
    table += THIRD_TAG
  end
  table += sumMoney.round.to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "円"
  table += FOURTH_TAG
  table += TABLE_FOOTER
  table += "<br />\n"
  table += "<br />\n"
  table += "<br />\n"
  return table
end

# ストラテジー別HTML作成
labelSerial = 1
outputFile = ''

if dbswingEURUSDrowArray.size > 0
  outputFile += createTable(dbswingEURUSDrowArray, DBSWING_EURUSD_LAST, labelSerial) 
  labelSerial += 1
end

if tfollowEURUSDrowArray.size > 0
  outputFile += createTable(tfollowEURUSDrowArray, T_Follow_EURUSD_LAST, labelSerial) 
  labelSerial += 1
end

if pminvestcapitalEURGBProwArray.size > 0
  outputFile += createTable(pminvestcapitalEURGBProwArray, PMINVESTCAPITAL_EURGBP_LAST, labelSerial) 
  labelSerial += 1
end

if rikiwikitavyfxUSDJPYrowArray.size > 0
  outputFile += createTable(rikiwikitavyfxUSDJPYrowArray, RIKIWIKITAVYFX_USDJPY_LAST, labelSerial) 
  labelSerial += 1
end

# HTMLファイルの書き出し
outputHTMLFile = "<html><body>\n" 
outputHTMLFile += outputFile 
outputHTMLFile += "</body></html>\n"

open("シストレ24成績.html", "w") {|f| f.write outputFile}

book.close(false)
app.quit

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル(複数選択対応版)

前回作成したサンプルでは、リストボックスなのに複数選択に対応していない。HTML的に複数選択に対応しても、DBにうまくinsertされない。

しかも、手動でGameクラスにLineupクラスを複数追加すると、下図のようにリストボックスが複数表示されてしまって見栄えが悪い。


下記のサイトで同じ課題に取り組んでいる人がいたので、参考にさせていただいた。


Complex Forms for Many-to-Many Relationships
http://beacon.wharton.upenn.edu/learning/2012/02/complex-forms-for-many-to-many-relationships/

まずは改良版のGameクラスとLineupクラスのscaffold。(実行結果は省略。)
rails g scaffold bettergame date:date
rails g scaffold betterlineup bettergame:references player:references
rake db:migrate
次にbetterゲームクラスのmodelを下記のように修正する。
# encoding: utf-8

class Bettergame < ActiveRecord::Base
  has_many :players, :through => :betterlineups
  has_many :betterlineups
  accepts_nested_attributes_for :betterlineups

  def player_names
    players.collect{|p| p.name}.join(', ')
  end

  # PlayerクラスのIDを配列でget/setできる属性を追加
  def player_ids
    @player_ids || players.collect{|p| p.id}
  end
 
  def player_ids=(id_array)
    @player_ids = id_array.collect{|id| id.to_i};
  end

  after_save :assign_players

  private

  # PlayerクラスのIDの配列から、Betterlineupクラスを追加/編集/削除する。
  def assign_players
    if @player_ids
      new_ids = @player_ids
      old_ids = players.collect{|p| p.id}
      ids_to_delete = old_ids - (old_ids & new_ids)
      ids_to_add = new_ids - (old_ids & new_ids)
      bettergame_id = id
 
      ids_to_delete.each do |player_id|
        Betterlineup.destroy_all(:bettergame_id => bettergame_id, :player_id => player_id)
      end
 
      ids_to_add.each do |player_id|
        Betterlineup.create(:bettergame_id => bettergame_id, :player_id => player_id)
      end
    end
  end
end
リストボックスから複数送信されるPlayerのIDを配列としてまとめて扱うためのメソッドと、送信されたPlayerのIDとLineupクラスで既に保持しているPlayerのIDを比較して、追加/更新/削除を行うメソッドの2つが追加されている。

次に、viewの_form.html.erbを下記のように修正する。
<%= form_for(@bettergame) do |f| %>
  <% if @bettergame.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@bettergame.errors.count, "error") %> prohibited this bettergame from being saved:</h2>

      <ul>
      <% @bettergame.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true %>
  </div>
  <div class="field">
    <%= f.label :lineups %><br />
    <%= f.select(:player_ids, @player_select_data, {}, {:multiple => true, :size => Player.all.size}) %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
セレクトボックスのヘルパーには、先ほど modelクラスに追加した「:player_ids」を指定している。 その次の「@player_select_data」は、リストボックスに表示する値の配列となる。後で Controllerに追加する。 複数選択できるように、「:multiple => true」として、リストボックスの表示サイズは、Player全員とするために、「:size => Player.all.size」としている。

最後に、Controllerの修正は下記の通りとなる。(new~update部分のみ)
  # GET /bettergames/new
  # GET /bettergames/new.json
  def new
    @bettergame = Bettergame.new
    @player_select_data = Player.all.collect{|p| [p.name, p.id]}

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @bettergame }
    end
  end

  # GET /bettergames/1/edit
  def edit
    @bettergame = Bettergame.find(params[:id])
    @player_select_data = Player.all.collect{|p| [p.name, p.id]}
  end

  # POST /bettergames
  # POST /bettergames.json
  def create
    @bettergame = Bettergame.new(params[:bettergame])

    respond_to do |format|
      if @bettergame.save
        format.html { redirect_to @bettergame, notice: 'Bettergame was successfully created.' }
        format.json { render json: @bettergame, status: :created, location: @bettergame }
      else
        format.html { render action: "new" }
        format.json { render json: @bettergame.errors, status: :unprocessable_entity }
      end
    end
  end

  # PUT /bettergames/1
  # PUT /bettergames/1.json
  def update
    @bettergame = Bettergame.find(params[:id])

    respond_to do |format|
      if @bettergame.update_attributes(params[:bettergame])
        format.html { redirect_to @bettergame, notice: 'Bettergame was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: "edit" }
        format.json { render json: @bettergame.errors, status: :unprocessable_entity }
      end
    end
  end
修正箇所は、newとeditのメソッド内で、「@player_select_data」の設定(Playerクラスの全データ取得)を行なっているのみ。

これで、複数選択の追加/編集/削除がリストボックス上から行えるようになった。


2012年11月7日水曜日

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル


野球やサッカーなどで、手持ちの選手から試合ごとのメンバーを決める場合のクラス図は上図のようになる。Lineupという中間クラスを通して、GameクラスとPlayerクラスが多対多の関係になっている。

こういう構造の場合に、Gameクラスにデータを追加→Playerクラスにデータを追加→Lineupクラスにデータを追加というように、順々に画面を操作していくのは面倒である。

下図のように、GameクラスとLineupクラスを一画面で同時に追加することはできないだろうか?


まずはscaffold。(実行結果は省略。)
rails g scaffold game date:date
rails g scaffold player name:string
rails g scaffold lineup game:references player:references
rake db:migrate
次に、modelにクラス間のアソシエーションを設定する。
class Game < ActiveRecord::Base
  has_many :players, :through => :lineups
  has_many :lineups
  accepts_nested_attributes_for :lineups
end

class Lineup < ActiveRecord::Base
  belongs_to :game
  belongs_to :player
end
「has_many 子クラス :through 中間クラス」と書くことで、中間クラスを通じて子クラスの属性にアクセスすることができるようなる。

つまり、下記のように中間クラスLineupを意識せずに、子クラスPlayerを扱えるようになる。
irb(main):001:0> Game.find(1).players[0].name
=> "陽岱鋼"
「accepts_nested_attributes_for 中間クラス」と書くことで、子クラスの変更を親クラスで保存できるようになる。
irb(main):002:0> Game.find(1).players << Player.find(2)
irb(main):003:0> Game.find(1).save
irb(main):004:0> Game.find(1).players.last.name
=> "今浪"
view側の変更は下記のようになる。
<%= form_for(@game) do |f| %>
  <% if @game.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@game.errors.count, "error") %> prohibited this game from being saved:</h2>

      <ul>
      <% @game.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true  %>
  </div>
  <div class="field">
    <%= f.label :lineups %><br />
    <%= f.fields_for :lineups do |lf| %>
      <%= lf.hidden_field :id %>
      <%= lf.select :player_id, Player.all.map {|player| [player.name, player.id]}, {:selected=>lf.object.player_id}, :size => Player.all.size %>
    <% end %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
「fields_for」の内側に子クラスのform要素を記述する。
これで1つのview内に複数のmodelを扱えるようになる。

「:selected=>lf.object.player_id」は、こう記述しておくと、表示しているPlayerクラスの一覧から、Lineupクラスに登録されているものが選択された状態になる。

次に、controller側の変更は下記のようになる。
  # GET /games/new
  # GET /games/new.json
  def new
    @game = Game.new
    @game.lineups.build

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @game }
    end
  end
「@game.lineups.build」の行を追加したのみ。
ここでbuildをしておかないと、view側のfields_forで指定している:lineupsが空になってしまうために、下図のようにリストボックスが表示されなくなってしまう。


model、view、controllerの修正後は、下図のようにGameクラスの画面から、子クラスPlayerの追加、修正ができるようになる。


しかしながら、現状では、せっかくリストボックスを使っているのに複数選択に対応していない。
実質的にコンボボックスと同じ機能になってしまっている。

複数選択の対応は次回の課題としたい。

以下は、今回参考にさせていただいたブログ。

has_manyな関連先をまとめてINSERTする
http://aerial.st/archive/2011/06/11/insert-has-many-relations/

has_manyな関連で、1アクションで複数のモデルを同時に保存するには?
http://d.hatena.ne.jp/zariganitosh/20080104/1199437301

[Rails]accepts_nested_attributes_forでネストした別モデルのフォーム[Ruby]
http://shasou.blogspot.jp/2011/05/railsacceptsnestedattributesforruby.html

Nested AttributesとNested Model Formsを使って親子オブジェクトを一括で登録/変更するには
http://www.everyleaf.com/tech/ror_tips/nested-attributesnested-model-forms/



2012年11月2日金曜日

Railsで正規化されていないIDから関連テーブルの情報を取得するサンプル


上図の例のように、正規化せずに一つのテーブル内に複数の外部キーを持った場合に、外部のテーブルの情報を参照(Food.name)するにはどうすれば良いだろうか?

なにはともあれscaffold。(実行結果は省略。)
rails g scaffold food name:string
rails g scaffold person name:string first_favfood_id:integer second_favfood_id:integer third_favfood_id:integer
rake db:migrate
FoodとPersonに適当にデータを入力しておく。



通常は、下記のように参照先のテーブルをblongs_toで指定することになるのだが、
class Person < ActiveRecord::Base
  belongs_to :food
end
こうすると外部キーのIDが、food_idに限定されてしまう。

下記のようにbelongs_toのオプションを指定すると、food_idの外部キーを個別に指定できるようだ。
class Person < ActiveRecord::Base
  belongs_to :first_favfood, :class_name => 'Food', :foreign_key => 'first_favfood_id'
  belongs_to :second_favfood, :class_name => 'Food', :foreign_key => 'second_favfood_id'
  belongs_to :third_favfood, :class_name => 'Food', :foreign_key => 'third_favfood_id'
end
その後、viewのindex.html.erbを下記のように変更すると、Food.nameが参照できるようになった。
<h1>Listing people</h1>

<table>
  <tr>
    <th>Name</th>
    <th>First favfood</th>
    <th>Second favfood</th>
    <th>Third favfood</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @people.each do |person| %>
  <tr>
    <td><%= person.name %></td>
    <td><%= person.first_favfood.name %></td>
    <td><%= person.second_favfood.name %></td>
    <td><%= person.third_favfood.name %></td>
    <td><%= link_to 'Show', person %></td>
    <td><%= link_to 'Edit', edit_person_path(person) %></td>
    <td><%= link_to 'Destroy', person, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New Person', new_person_path %>

下記のブログが参考になった。

【Ruby on Rails】1つのテーブルと2フィールドでリレーションを持たせる方法(作成者と最終更新者の情報を持たせる)
http://kagayoshito.com/posts/show/102