class SongsController < BaseController
-def index
-end
+ def index
+ end
-def new
- @song = Song.new
-end
+ def new
+ @song = Song.new
+ @song.font_size = Song::DEFAULT_FONT_SIZE
+ end
+
+ def create
+ render :action => :new unless params[:song]
+ @song = Song.new(params[:song])
+ unless @song.valid?
+ p @song.errors
+ p @song.valid?
+ flash[:error] = "error desu"
+ return render :action => :new
+ end
+ unless @song.save
+ flash[:error] = "保存できませんでした"
+ end
+ render :action => :edit
+ end
+ def edit
+ @song = Song.find_by_id(params[:id])
+ redirect_to songs_path if @song.blank?
+ end
end
class Song < ActiveRecord::Base
+ include ::SelectableAttr::Base
+
+ DEFAULT_FONT_SIZE = 2
+ CODES = %w(A Ab Bb C Cm D E Eb Em F F#m Fm G)
+
+ selectable_attr :font_size do
+ entry "0", :xsmall, 'XSmall'
+ entry "1", :small, 'Small'
+ entry "2", :middle, 'Middle'
+ entry "3", :large, 'Large'
+ entry "4", :xlarge, 'XLarge'
+ end
+
+# validates_presence_of :titile, :words, :font_size
+# validates_numericality_of :font_size
+ def kana
+ self.words
+ end
end
--- /dev/null
+<%= f.error_messages %>
+<table>
+ <tr>
+ <th>コード(CODE)</th>
+ <td><%= f.select :code, Song::CODES, :include_blank => true %></td>
+ </tr>
+ <tr>
+ <th>題名(TITLE)</th>
+ <td><%= f.text_field :title %></td>
+ </tr>
+ <tr>
+ <th>歌詞(WORDS)</th>
+ <td><%= f.text_area :words %></td>
+ </tr>
+ <tr>
+ <th>出典(SOURCE)</th>
+ <td><%= f.text_field :copyright %></td>
+ </tr>
+ <tr>
+ <th>表示サイズ(FONT SIZE)</th>
+ <td><%= f.select :font_size, Song.font_size_options %></td>
+ </tr>
+ <tr>
+ <td colspan="2"><%= f.submit "保存する(SAVE)" %></td>
+ </tr>
+</table>
--- /dev/null
+<% content_for :head do -%>
+ <title>Praise DATABASE</title>
+ <%= javascript_include_tag "prototype" %>
+ <%= javascript_include_tag "roman" %>
+<% end -%>
+<h1>歌詞の編集 (edit this song)</h1>
+
+<% form_for @song, song_path(@song), :method => :put do |f| -%>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= f.text_area :kana, :style => "display:none" %>
+<% end -%>
+
+<% form_for @song, song_path, :method => :delete do |f| -%>
+ <p>このデータを<%= f.submit "削除する" %></p>
+<% end -%>
+[<%= link_to "表示", song_path(@song) %>]
+[<%= link_to "印刷用画面", print_song_path(@song) %>]
+[<%= link_to "トップに戻る", song_path %>]
+[<%= link_to_function "ローマ字", "toRoman()" %>]
+[<%= link_to_function "リセット", "reset()" %>]
+<hr />
+
--- /dev/null
+<h1>歌詞の作成 (create new song)</h1>
+<p>歌詞その他を入力してください。ローマ字の登録は次の画面でローマ字ボタン押してください。</p>
+
+<p>
+ <%= flash[:notice] %>
+ <%= @error_messages %>
+</p>
+
+<% form_for @song, songs_path do |f| -%>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+<% end -%>
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')]
# config.i18n.default_locale = :de
-end
\ No newline at end of file
+end
ActionController::Routing::Routes.draw do |map|
# The priority is based upon order of creation: first created -> highest priority.
- map.resources :songs, :collection => { :search => [:get], :list => [:get] }
+ map.resources :songs,
+ :collection => { :search => [:get], :list => [:get] },
+ :member => { :print => [:get] }
# Sample of regular route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
t.text :words, :null => false, :default => "", :comment => "歌詞本体"
t.text :words_for_search, :null => false, :default => "", :comment => "歌詞検索用のデータ"
t.string :copyright, :null => false, :default => "", :comment => "楽曲の出典"
- t.int :font_size, :null => false, :default => 4, :comment => "表示フォントサイズ"
+ t.integer :font_size, :null => false, :default => 2, :comment => "表示フォントサイズ"
t.datetime :deleted_at, :comment => "削除タイムスタンプ"
t.timestamps
--- /dev/null
+\r
+var src;\r
+var kana;\r
+var box;\r
+\r
+function init() {\r
+ box = $('song_words');\r
+ src = box.value;\r
+ kana = $('song_kana').value;\r
+}\r
+\r
+function reset() {\r
+ box.value = src;\r
+}\r
+\r
+function toRoman() {\r
+ var result = "";\r
+ var roman = new Roman();\r
+ var srcLines = src.split("\n"); \r
+ var kanaLines = kana.split("\n");\r
+ for (var i = 0; i < srcLines.length; i++) {\r
+ result += srcLines[i] + "\n";\r
+ if (srcLines[i].match(/^\s*$/)) continue;\r
+ result += roman.conv(kanaLines[i]) + "\n";\r
+ }\r
+ box.value = result;\r
+}\r
+\r
+function Roman(convTable) {\r
+ var kanaTable = [];\r
+ var romaTable = [];\r
+ for (var i = 0; i < tbl.length; i+=2) {\r
+ kanaTable.push(new RegExp(tbl[i], "g"));\r
+ romaTable.push(tbl[i+1]);\r
+ }\r
+ function convert(str) {\r
+ var result = str;\r
+ for (var i = 0; i < kanaTable.length; i++) {\r
+ result = result.replace(kanaTable[i], romaTable[i]);\r
+ }\r
+ result = result.replace(/[ ]+/g, " ");\r
+ result = result.replace(/^ /mg, "");\r
+ return result;\r
+ }\r
+ \r
+ return {"conv": convert};\r
+}\r
+\r
+var tbl =\r
+[\r
+\r
+ // 特殊表記(固有名詞) かなの長さ順に並べる\r
+\r
+ "インマヌエル","EMMANUEL",\r
+ "ハレルーヤー","HALLELUJAH",\r
+\r
+ "クリスマス","CHRISTMAS",\r
+ "グローリー","GLORY",\r
+ "リバイバル","REVIVAL",\r
+ "ファミリー","FAMILY",\r
+\r
+ "アーメン","AMEN",\r
+ "アルファ","ALPHA",\r
+ "グローリ","GLORY",\r
+ "ジーザス","JESUS",\r
+ "しょうり","SHORI",\r
+ "ハレルヤ","HALLELUJAH",\r
+ "メサイヤ","MESSIAH",\r
+ "キリスト","KIRISUTO",\r
+\r
+ "メシヤ","MESSIAH",\r
+ "オメガ","OMEGA",\r
+ "ホサナ","HOSANNA",\r
+ "ホザナ","HOSANNA",\r
+ "ロード","LORD",\r
+ "イエス","IESU",\r
+\r
+\r
+ // 促音の特例:っち、っちゃ、っちゅ、っちょ\r
+\r
+ "っちゃ","TCHA",\r
+ "っちゅ","TCHU",\r
+ "っちょ","TCHO",\r
+ "っち","TCHI",\r
+\r
+ "ッチャ","TCHA",\r
+ "ッチュ","TCHU",\r
+ "ッチョ","TCHO",\r
+ "ッチ","TCHI",\r
+\r
+\r
+ // 拗音のうしろの「あ」「う」は消える\r
+ // 例:しょうり → ○SHORI ×SHOURI\r
+\r
+ "([きしちにひみりぎじぢ]ゃ)あ","$1",\r
+ "([きしちにひみりぎじぢ][ゅょ])う","$1",\r
+\r
+ "([キシチニヒミリギジヂ]ャ)ア","$1",\r
+ "([キシチニヒミリギジヂ][ュョ])ウ","$1",\r
+\r
+\r
+ // 拗音\r
+\r
+ "きゃ","KYA",\r
+ "きゅ","KYU",\r
+ "きょ","KYO",\r
+\r
+ "しゃ","SHA",\r
+ "しゅ","SHU",\r
+ "しょ","SHO",\r
+\r
+ "ちゃ","CHA",\r
+ "ちゅ","CHU",\r
+ "ちょ","CHO",\r
+\r
+ "にゃ","NYA",\r
+ "にゅ","NYU",\r
+ "にょ","NYO",\r
+\r
+ "ひゃ","HYA",\r
+ "ひゅ","HYU",\r
+ "ひょ","HYO",\r
+\r
+ "みゃ","MYA",\r
+ "みゅ","MYU",\r
+ "みょ","MYO",\r
+\r
+ "りゃ","RYA",\r
+ "りゅ","RYU",\r
+ "りょ","RYO",\r
+\r
+ "ぎゃ","GYA",\r
+ "ぎゅ","GYU",\r
+ "ぎょ","GYO",\r
+\r
+ "じゃ","JYA",\r
+ "じゅ","JYU",\r
+ "じょ","JYO",\r
+\r
+ "キャ","KYA",\r
+ "キュ","KYU",\r
+ "キョ","KYO",\r
+\r
+ "シャ","SHA",\r
+ "シュ","SHU",\r
+ "シュ","SHU",\r
+ "ショ","SHO",\r
+\r
+ "チャ","CHA",\r
+ "チュ","CHU",\r
+ "チョ","CHO",\r
+\r
+ "ニャ","NYA",\r
+ "ニュ","NYU",\r
+ "ニョ","NYO",\r
+\r
+ "ヒャ","HYA",\r
+ "ヒュ","HYU",\r
+ "ヒョ","HYO",\r
+\r
+ "ミャ","MYA",\r
+ "ミュ","MYU",\r
+ "ミョ","MYO",\r
+\r
+ "リャ","RYA",\r
+ "リュ","RYU",\r
+ "リョ","RYO",\r
+\r
+ "ギャ","GYA",\r
+ "ギュ","GYU",\r
+ "ギョ","GYO",\r
+ "ジャ","JYA",\r
+ "ジュ","JYU",\r
+ "ジョ","JYO",\r
+\r
+\r
+ //「おう」はHで伸ばす\r
+ // 例:「王」→「おう」→「OH」\r
+\r
+ "おう","OH",\r
+\r
+\r
+ // それ以外の「おの段+う」は「う」を無視する\r
+ // 例:「栄光」→「えいこう」→「EIKO」\r
+\r
+ "([こそとのほもよろを])う","$1",\r
+\r
+\r
+ // アの段、オの段の長音はHで伸ばす\r
+ // 例:"サー" → "サH" → "SAH"\r
+ // "コー" → "コH" → "KOH"\r
+\r
+ "([アカサタナハマヤラワ])ー","$1H",\r
+ "([ガザダバ])ー","$1H",\r
+\r
+ "([オコソトノホモヨロヲ])ー","$1H",\r
+ "([ゴゾドボ])ー","$1H",\r
+\r
+ // それ以外の長音は無視する\r
+\r
+ "(.)ー","$1",\r
+\r
+\r
+ //「ん」の直後の母音はハイフンで分ける\r
+\r
+ "んあ","N-A",\r
+ "んい","N-I",\r
+ "んう","N-U",\r
+ "んえ","N-E",\r
+ "んお","N-O",\r
+\r
+ "ンア","N-A",\r
+ "ンイ","N-I",\r
+ "ンウ","N-U",\r
+ "ンエ","N-E",\r
+ "ンオ","N-O",\r
+\r
+\r
+ // 通常のかな\r
+\r
+ "あ","A",\r
+ "い","I",\r
+ "う","U",\r
+ "え","E",\r
+ "お","O",\r
+\r
+ "か","KA",\r
+ "き","KI",\r
+ "く","KU",\r
+ "け","KE",\r
+ "こ","KO",\r
+\r
+ "が","GA",\r
+ "ぎ","GI",\r
+ "ぐ","GU",\r
+ "げ","GE",\r
+ "ご","GO",\r
+\r
+ "さ","SA",\r
+ "し","SHI",\r
+ "す","SU",\r
+ "せ","SE",\r
+ "そ","SO",\r
+\r
+ "ざ","ZA",\r
+ "じ","JI",\r
+ "ず","ZU",\r
+ "ぜ","ZE",\r
+ "ぞ","ZO",\r
+\r
+ "た","TA",\r
+ "ち","CHI",\r
+ "つ","TSU",\r
+ "て","TE",\r
+ "と","TO",\r
+\r
+ "だ","DA",\r
+ "ぢ","JI",\r
+ "づ","ZU",\r
+ "で","DE",\r
+ "ど","DO",\r
+\r
+ "な","NA",\r
+ "に","NI",\r
+ "ぬ","NU",\r
+ "ね","NE",\r
+ "の","NO",\r
+\r
+ "は","HA",\r
+ "ひ","HI",\r
+ "ふ","FU",\r
+ "へ","HE",\r
+ "ほ","HO",\r
+\r
+ "ば","BA",\r
+ "び","BI",\r
+ "ぶ","BU",\r
+ "べ","BE",\r
+ "ぼ","BO",\r
+\r
+ "ぱ","PA",\r
+ "ぴ","PI",\r
+ "ぷ","PU",\r
+ "ぺ","PE",\r
+ "ぽ","PO",\r
+\r
+ "ま","MA",\r
+ "み","MI",\r
+ "む","MU",\r
+ "め","ME",\r
+ "も","MO",\r
+\r
+ "や","YA",\r
+ "ゆ","YU",\r
+ "よ","YO",\r
+\r
+ "ら","RA",\r
+ "り","RI",\r
+ "る","RU",\r
+ "れ","RE",\r
+ "ろ","RO",\r
+\r
+ "わ","WA",\r
+ "を","O",\r
+\r
+ "ア","A",\r
+ "イ","I",\r
+ "ウ","U",\r
+ "エ","E",\r
+ "オ","O",\r
+\r
+ "カ","KA",\r
+ "キ","KI",\r
+ "ク","KU",\r
+ "ケ","KE",\r
+ "コ","KO",\r
+\r
+ "ガ","GA",\r
+ "ギ","GI",\r
+ "グ","GU",\r
+ "ゲ","GE",\r
+ "ゴ","GO",\r
+\r
+ "サ","SA",\r
+ "シ","SHI",\r
+ "ス","SU",\r
+ "セ","SE",\r
+ "ソ","SO",\r
+\r
+ "ザ","ZA",\r
+ "ジ","JI",\r
+ "ズ","ZU",\r
+ "ゼ","ZE",\r
+ "ゾ","ZO",\r
+\r
+ "タ","TA",\r
+ "チ","CHI",\r
+ "ツ","TSU",\r
+ "テ","TE",\r
+ "ト","TO",\r
+\r
+ "ダ","DA",\r
+ "ヂ","JI",\r
+ "ヅ","ZU",\r
+ "デ","DE",\r
+ "ド","DO",\r
+\r
+ "ナ","NA",\r
+ "ニ","NI",\r
+ "ヌ","NU",\r
+ "ネ","NE",\r
+ "ノ","NO",\r
+\r
+ "ハ","HA",\r
+ "ヒ","HI",\r
+ "フ","FU",\r
+ "ヘ","HE",\r
+ "ホ","HO",\r
+\r
+ "バ","BA",\r
+ "ビ","BI",\r
+ "ブ","BU",\r
+ "ベ","BE",\r
+ "ボ","BO",\r
+\r
+ "パ","PA",\r
+ "ピ","PI",\r
+ "プ","PU",\r
+ "ペ","PE",\r
+ "ポ","PO",\r
+\r
+ "マ","MA",\r
+ "ミ","MI",\r
+ "ム","MU",\r
+ "メ","ME",\r
+ "モ","MO",\r
+\r
+ "ヤ","YA",\r
+ "ユ","YU",\r
+ "ヨ","YO",\r
+\r
+ "ラ","RA",\r
+ "リ","RI",\r
+ "ル","RU",\r
+ "レ","RE",\r
+ "ロ","RO",\r
+\r
+ "ワ","WA",\r
+ "ヲ","O",\r
+\r
+ // 撥音の特例:B,M,Pの前にはNの代わりにMをおく\r
+ // それ以外はNとする\r
+\r
+ "[んン]([BMP])","M$1",\r
+ "[んン]","N",\r
+\r
+\r
+ // 促音は次の子音を重ねる(特例は定義済み)\r
+ // ※ここまでの変換でその後ろのかなは\r
+ // ローマ字に変換されているという前提\r
+\r
+ "[っッ](.)","$1$1",\r
+\r
+\r
+ // 全角スペースを半角スペースに\r
+\r
+ " "," "\r
+];\r
+\r
+Event.observe(window, "load", init);\r
--- /dev/null
+* {
+ scrollbar-base-color: #223355;
+}
+body {
+ background-color: #005;
+ background-image: url(/images/TLCblue.png);
+ background-attachment: fixed;
+ color: white;
+}
+a:link {
+ color: white;
+ text-decoration: none;
+}
+a:visited {
+ color: white;
+ text-decoration: none;
+}
+a:hover {
+ background-color: red;
+ text-decoration: underline;
+}
+a:active {
+ color: red;
+ text-decoration: none;
+}
--- /dev/null
+Copyright (c) 2008 Takeshi AKIMA
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+= SelectableAttr
+== Introduction
+selectable_attr は、コードが割り振られるような特定の属性について*コード*、*プログラム上での名前*、
+*表示するための名前*などをまとめて管理するものです。
+http://github.com/akm/selectable_attr/tree/master
+
+Railsで使用する場合、selectable_attr_railsと一緒に使うことをオススメします。
+http://github.com/akm/selectable_attr_rails/tree/master
+
+
+== Install
+=== 1. Railsプロジェクトで使う場合
+==== a. plugin install
+ ruby script/plugin install git://github.com/akm/selectable_attr.git
+ ruby script/plugin install git://github.com/akm/selectable_attr_rails.git
+
+==== b. gem install
+ [sudo] gem install akimatter-selectable_attr akimatter-selectable_attr_rails -s http://gems.github .com
+
+=== 2. 非Railsで使う場合
+==== a. gem install
+ [sudo] gem install akimatter-selectable_attr -s http://gems.github .com
+
+
+
+== Tutorial
+
+=== シンプルなパターン
+ require 'rubygems'
+ require 'selectable_attr'
+
+ class Person
+ include ::SelectableAttr::Base
+
+ selectable_attr :gender do
+ entry '1', :male, '男性'
+ entry '2', :female, '女性'
+ entry '9', :other, 'その他'
+ end
+ end
+
+上記のようなクラスがあった場合、以下のようなクラスメソッドを使うことが可能です。
+
+ irb(main):020:0> Person.gender_ids
+ => ["1", "2", "9"]
+ irb(main):021:0> Person.gender_keys
+ => [:male, :female, :other]
+ irb(main):022:0> Person.gender_names
+ => ["男性", "女性", "その他"]
+ irb(main):023:0> Person.gender_options
+ => [["男性", "1"], ["女性", "2"], ["その他", "9"]] # railsでoptions_for_selectメソッドなどに使えます。
+ irb(main):024:0> Person.gender_key_by_id("1")
+ => :male
+ irb(main):025:0> Person.gender_name_by_id("1")
+ => "男性"
+ irb(main):026:0> Person.gender_id_by_key(:male) # 特定のキーから対応するidを取得できます。
+ => "1"
+ irb(main):027:0> Person.gender_name_by_key(:male)
+ => "男性"
+
+また使用可能なインスタンスメソッドには以下のようなものがあります。
+
+ irb> person = Person.new
+ => #<Person:0x133b9d0>
+ irb> person.gender_key
+ => nil
+ irb> person.gender_name
+ => nil
+ irb> person.gender = "2"
+ => "2"
+ irb> person.gender_key
+ => :female
+ irb> person.gender_name
+ => "女性"
+ irb> person.gender_key = :other
+ => :other
+ irb> person.gender
+ => "9"
+ irb> person.gender_name
+ => "その他"
+
+genderが代入可能なことはもちろん、gender_keyも代入可能です。
+# ただし、gender_nameには代入できません。
+
+
+=== 複数の値を取りうるパターン
+ require 'rubygems'
+ require 'selectable_attr'
+
+ class RoomSearch
+ include ::SelectableAttr::Base
+
+ multi_selectable_attr :room_type do
+ entry '01', :single, 'シングル'
+ entry '02', :twin, 'ツイン'
+ entry '03', :double, 'ダブル'
+ entry '04', :triple, 'トリプル'
+ end
+ end
+
+multi_selectable_attrを使った場合に使用できるクラスメソッドは、selectable_attrの場合と同じです。
+
+ irb> room_search = RoomSearch.new
+ => #<RoomSearch:0x134a070>
+ irb> room_search.room_type_ids
+ => []
+ irb> room_search.room_type_keys
+ => []
+ irb> room_search.room_type_names
+ => []
+ irb> room_search.room_type_selection
+ => [false, false, false, false]
+ irb> room_search.room_type_keys = [:twin, :double]
+ => [:twin, :double]
+ irb> room_search.room_type
+ => ["02", "03"]
+ irb> room_search.room_type_names
+ => ["ツイン", "ダブル"]
+ irb> room_search.room_type_ids
+ => ["02", "03"]
+ irb> room_search.room_type = ["01", "04"]
+ => ["01", "04"]
+ irb> room_search.room_type_keys
+ => [:single, :triple]
+ irb> room_search.room_type_names
+ => ["シングル", "トリプル"]
+ irb> room_search.room_type_selection
+ => [true, false, false, true]
+ irb> room_search.room_type_hash_array
+ => [{:select=>true, :key=>:single, :name=>"シングル", :id=>"01"}, {:select=>false, :key=>:twin, :name=>"ツイン", :id=>"02"}, {:select=>false, :key=>:double, :name=>"ダブル", :id=>"03"}, {:select=>true, :key=>:triple, :name=>"トリプル", :id=>"04"}]
+ irb> room_search.room_type_hash_array_selected
+ => [{:select=>true, :key=>:single, :name=>"シングル", :id=>"01"}, {:select=>true, :key=>:triple, :name=>"トリプル", :id=>"04"}]
+
+
+
+=== Entry
+==== エントリの取得
+エントリは様々な拡張が可能です。例えば以下のようにid、key、name以外の属性を設定することも可能です。
+
+ require 'rubygems'
+ require 'selectable_attr'
+
+ class Site
+ include ::SelectableAttr::Base
+
+ selectable_attr :protocol do
+ entry '01', :http , 'HTTP' , :port => 80
+ entry '02', :https, 'HTTPS' , :port => 443
+ entry '03', :ssh , 'SSH' , :port => 22
+ entry '04', :svn , 'Subversion', :port => 3690
+ end
+ end
+
+クラスメソッドで各エントリを取得することが可能です。
+ entry = Site.protocol_entry_by_key(:https)
+ entry = Site.protocol_entry_by_id('02')
+
+インスタンスメソッドでは以下のように取得できます。
+ site = Site.new
+ site.protocol_key = :https
+ entry = site.protocol_entry
+
+==== エントリの属性
+id, key, nameもそのままメソッドとして用意されています。
+ irb> entry.id
+ => "02"
+ irb> entry.key
+ => :https
+ irb> entry.name
+ => "HTTPS"
+
+またオプションの属性もHashのようにアクセス可能です。
+ irb> entry[:port]
+ => 443
+
+to_hashメソッドで、id, key, nameを含むHashを作成します。
+ irb> entry.to_hash
+ => {:key=>:https, :port=>443, :name=>"HTTPS", :id=>"02"}
+
+matchメソッドでid,key,nameを除くオプションの属性群と一致しているかどうかを判断可能です。
+
+ irb> entry.match?(:port => 22)
+ => false
+ irb> entry.match?(:port => 443)
+ => true
+
+ # ここではオプションの属性として:portしか設定していないので、matchに渡すHashのキーと値の組み合わせも一つだけですが、
+ # 複数ある場合にmatch?がtrueとなるためには、完全に一致している必要があります。
+
+
+==== クラスメソッドでのエントリの扱い
+クラスメソッドでエントリを取得する方法として、xxx_entry_by_id, xxx_entry_by_keyを紹介しましたが、
+全てのエントリで構成される配列を取得するメソッドが xxx_entriesです。
+
+ irb> entries = Site.protocol_entries
+ irb> entries.length
+ => 4
+
+また、各エントリをto_hashでHashに変換した配列を xxx_hash_arrayメソッドで取得することも可能です。
+ irb> Site.protocol_hash_array
+ => [
+ {:key=>:http, :port=>80, :name=>"HTTP", :id=>"01"},
+ {:key=>:https, :port=>443, :name=>"HTTPS", :id=>"02"},
+ {:key=>:ssh, :port=>22, :name=>"SSH", :id=>"03"},
+ {:key=>:svn, :port=>3690, :name=>"Subversion", :id=>"04"}
+ ]
+
+
+=== Enum
+あまり表にでてきませんが、エントリをまとめる役割のオブジェクトがEnumです。
+これはクラスメソッドxxx_enumで取得することができます。
+
+ irb> enum = Site.protocol_enum
+
+Enumには以下のようなメソッドが用意されています。
+ irb> enum.entries
+ irb> enum.entries.map{|entry| entry[:port]}
+ => [80, 443, 22, 3690]
+
+EnumはEnumerableをincludeしているため、以下のように記述することも可能です。
+ irb> enum.map{|entry| entry[:port]}
+ => [80, 443, 22, 3690]
+
+ irb> enum.entry_by_id("03")
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+ irb> enum.entry_by_key(:ssh)
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+ irb> enum.entry_by_id_or_key(:ssh)
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+ irb> enum.entry_by_id_or_key('03')
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+ irb> enum.entry_by_hash(:port => 22)
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+
+また、これらのメソッドが面倒と感じるようであれば、以下のような簡単なアクセスも可能です。
+ irb> enum['03']
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+ irb> enum[:ssh]
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+ irb> enum[:port => 22]
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+
+またクラスメソッドで紹介したようなxxx_ids, xxx_keys, xxx_namesや、xxx_key_by_idなどのメソッドも用意されています。
+ irb> enum.ids
+ => ["01", "02", "03", "04"]
+ irb> enum.keys
+ => [:http, :https, :ssh, :svn]
+ irb> enum.names
+ => ["HTTP", "HTTPS", "SSH", "Subversion"]
+ irb> enum.options
+ => [["HTTP", "01"], ["HTTPS", "02"], ["SSH", "03"], ["Subversion", "04"]]
+ irb> enum.key_by_id('04')
+ => :svn
+ irb> enum.id_by_key(:svn)
+ => "04"
+ irb> enum.name_by_id('04')
+ => "Subversion"
+ irb> enum.name_by_key(:svn)
+ => "Subversion"
+ irb> enum.to_hash_array
+ => [
+ {:key=>:http, :port=>80, :name=>"HTTP", :id=>"01"},
+ {:key=>:https, :port=>443, :name=>"HTTPS", :id=>"02"},
+ {:key=>:ssh, :port=>22, :name=>"SSH", :id=>"03"},
+ {:key=>:svn, :port=>3690, :name=>"Subversion", :id=>"04"}
+ ]
+
+id, key以外でエントリを特定したい場合はfindメソッドが使えます。
+ irb> enum.find(:port => 22)
+ => #<SelectableAttr::Enum::Entry:1352a54 @id="03", @key=:ssh, @name="SSH", @options={:port=>22}
+
+findメソッドにはブロックを渡すこともできます。
+ irb> enum.find{|entry| entry[:port] > 1024}
+ => #<SelectableAttr::Enum::Entry:1352a04 @id="04", @key=:svn, @name="Subversion", @options={:port=>3690}
+
+
+=== Entryへのメソッド定義
+entryメソッドにブロックを渡すとエントリのオブジェクトにメソッドを定義することが可能です。
+
+ require 'rubygems'
+ require 'selectable_attr'
+
+ class Site
+ include ::SelectableAttr::Base
+
+ selectable_attr :protocol do
+ entry '01', :http , 'HTTP', :port => 80 do
+ def accept?(model)
+ # httpで指定された場合はhttpsも可、という仕様
+ model.url =~ /^http[s]{0,1}\:\/\//
+ end
+ end
+
+ entry '02', :https, 'HTTPS', :port => 443 do
+ def accept?(model)
+ model.url =~ /^https\:\/\//
+ end
+ end
+
+ entry '03', :ssh , 'SSH', :port => 22 do
+ def accept?(model)
+ false
+ end
+ end
+
+ entry '04', :svn , 'Subversion', :port => 3690 do
+ def accept?(model)
+ model.url =~ /^svn\:\/\/|^svn+ssh\:\/\//
+ end
+ end
+
+ end
+ end
+
+ enum = Site.protocol_enum
+
+ class Project
+ attr_accessor :url
+ end
+ project = Project.new
+ project.url = "http://github.com/akm/selectable_attr/tree/master"
+
+ irb> enum[:http].accept?(project)
+ => 0
+ irb> enum[:https].accept?(project)
+ => nil
+
+ というようにentryメソッドに渡したブロックは、生成されるエントリオブジェクトのコンテキストでinstance_evalされるので、そのメソッドを定義することが可能です。
+
+
+
+
+== Credit
+Copyright (c) 2008 Takeshi AKIMA, released under the MIT lice nse
--- /dev/null
+require 'rubygems'
+gem 'rspec', '>= 1.1.4'
+require 'rake'
+require 'rake/rdoctask'
+require 'spec/rake/spectask'
+require 'spec/rake/verify_rcov'
+
+desc 'Default: run unit tests.'
+task :default => :spec
+
+task :pre_commit => [:spec, 'coverage:verify']
+
+desc 'Run all specs under spec/**/*_spec.rb'
+Spec::Rake::SpecTask.new(:spec => 'coverage:clean') do |t|
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ t.spec_opts = ["-c", "--diff"]
+ t.rcov = true
+ t.rcov_opts = ["--include-file", "lib\/*\.rb", "--exclude", "spec\/"]
+end
+
+desc 'Generate documentation for the selectable_attr plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'SelectableAttr'
+ rdoc.options << '--line-numbers' << '--inline-source' << '-c UTF-8'
+ rdoc.rdoc_files.include('README*')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+namespace :coverage do
+ desc "Delete aggregate coverage data."
+ task(:clean) { rm_f "coverage" }
+
+ desc "verify coverage threshold via RCov"
+ RCov::VerifyTask.new(:verify => :spec) do |t|
+ t.threshold = 100.0 # Make sure you have rcov 0.7 or higher!
+ t.index_html = 'coverage/index.html'
+ end
+end
+
+begin
+ require 'jeweler'
+ Jeweler::Tasks.new do |s|
+ s.name = "selectable_attr"
+ s.summary = "selectable_attr generates extra methods dynamically"
+ s.description = "selectable_attr generates extra methods dynamically for attribute which has options"
+ s.email = "akima@gmail.com"
+ s.homepage = "http://github.com/akm/selectable_attr/"
+ s.authors = ["Takeshi Akima"]
+ end
+rescue LoadError
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
+end
+
--- /dev/null
+---
+:patch: 11
+:major: 0
+:minor: 3
--- /dev/null
+require 'selectable_attr'
--- /dev/null
+# Install hook code here
--- /dev/null
+# -*- coding: utf-8 -*-
+require 'logger'
+
+module SelectableAttr
+ autoload :VERSION, 'selectable_attr/version'
+ autoload :Enum, 'selectable_attr/enum'
+ autoload :Base, 'selectable_attr/base'
+
+ class << self
+ def logger
+ @logger ||= Logger.new(STDERR)
+ end
+ def logger=(value)
+ @logger = value
+ end
+ end
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+module SelectableAttr
+ module Base
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ ENUM_ARRAY_METHODS = {
+ :none => {
+ :to_hash_array => Proc.new do |enum, attr_value|
+ value = (attr_value || []).map(&:to_s)
+ enum.to_hash_array do |hash|
+ hash[:select] = value.include?(hash[:id].to_s)
+ end
+ end,
+
+ :to_attr_value => Proc.new do |enum, hash_array|
+ hash_array.select{|hash| hash[:select]}.map{|hash| hash[:id]}
+ end
+ },
+
+ :comma_string => {
+ :to_hash_array => Proc.new do |enum, attr_value|
+ values = attr_value.is_a?(Array) ? attr_value.map{|v|v.to_s} :
+ (attr_value || '').split(',')
+ enum.to_hash_array do |hash|
+ hash[:select] = values.include?(hash[:id].to_s)
+ end
+ end,
+
+ :to_attr_value => Proc.new do |enum, hash_array|
+ hash_array.select{|hash| hash[:select]}.map{|hash| hash[:id]}.join(',')
+ end
+ },
+
+
+ :binary_string => {
+ :to_hash_array => Proc.new do |enum, attr_value|
+ value = attr_value || ''
+ idx = 0
+ enum.to_hash_array do |hash|
+ hash[:select] = (value[idx, 1] == '1')
+ idx += 1
+ end
+ end,
+
+ :to_attr_value => Proc.new do |enum, hash_array|
+ result = ''
+ hash_map = hash_array.inject({}){|dest, hash| dest[hash[:id]] = hash; dest}
+ enum.each do |entry|
+ hash = hash_map[entry.id]
+ result << (hash[:select] ? '1' : '0')
+ end
+ result
+ end
+ }
+ }
+
+ module ClassMethods
+ def single_selectable_attrs
+ @single_selectable_attrs_hash ||= {};
+ @single_selectable_attrs_hash[self] ||= []
+ end
+
+ def multi_selectable_attrs
+ @multi_selectable_attrs_hash ||= {};
+ @multi_selectable_attrs_hash[self] ||= []
+ end
+
+ def selectable_attr_type_for(attr)
+ single_selectable_attrs.include?(attr.to_s) ? :single :
+ multi_selectable_attrs.include?(attr.to_s) ? :multi : nil
+ end
+
+ def enum(*args, &block)
+ process_definition(block, *args) do |enum, context|
+ self.single_selectable_attrs << context[:attr].to_s
+ define_enum_class_methods(context)
+ define_enum_instance_methods(context)
+ end
+ end
+ alias_method :single_selectable_attr, :enum
+ alias_method :selectable_attr, :enum
+
+
+ def enum_array(*args, &block)
+ base_options = args.last.is_a?(Hash) ? args.pop : {}
+ args << base_options # .update({:attr_accessor => false})
+ process_definition(block, *args) do |enum, context|
+ self.multi_selectable_attrs << context[:attr].to_s
+ define_enum_class_methods(context)
+ define_enum_array_instance_methods(context)
+ end
+ end
+ alias_method :multi_selectable_attr, :enum_array
+
+ def process_definition(block, *args)
+ base_options = args.last.is_a?(Hash) ? args.pop : {}
+ enum = base_options[:enum] || create_enum(&block)
+ args.each do |attr|
+ context = {
+ :enum => enum,
+ :attr_accessor => !has_attr(attr),
+ :attr => attr,
+ :base_name => enum_base_name(attr)
+ }.update(base_options)
+ define_enum(context)
+ define_accessor(context)
+ yield(enum, context)
+ unless enum.i18n_scope
+ paths = [:selectable_attrs] + self.name.to_s.split('::').map{|s| s.to_sym}
+ paths << attr.to_sym
+ enum.i18n_scope(*paths)
+ end
+ end
+ enum
+ end
+
+ def has_attr(attr)
+ return true if self.method_defined?(attr)
+ return false unless self.respond_to?(:columns)
+ if self.respond_to?(:connection) and self.respond_to?(:connected?)
+ begin
+ self.connection unless self.connected?
+ rescue Exception
+ return nil if !self.connected?
+ end
+ end
+ (respond_to?(:table_exists?) && self.table_exists?) ?
+ (self.columns || []).any?{|col|col.name.to_s == attr.to_s} : false
+ end
+
+ def attr_enumeable_base(*args, &block)
+ @base_name_processor = block
+ end
+
+ def enum_base_name(attr)
+ if @base_name_processor
+ @base_name_processor.call(attr).to_s
+ else
+ attr.to_s.gsub(selectable_attr_name_pattern, '')
+ end
+ end
+
+ DEFAULT_SELECTABLE_ATTR_NAME_PATTERN = /(_cd$|_code$|_cds$|_codes$)/
+
+ def selectable_attr_name_pattern
+ @selectable_attr_name_pattern ||= DEFAULT_SELECTABLE_ATTR_NAME_PATTERN
+ end
+ alias_method :enum_name_pattern, :selectable_attr_name_pattern
+
+ def selectable_attr_name_pattern=(value)
+ @selectable_attr_name_pattern = value
+ end
+ alias_method :enum_name_pattern=, :selectable_attr_name_pattern=
+
+ def create_enum(&block)
+ result = Enum.new
+ result.instance_eval(&block)
+ result
+ end
+
+ def define_enum(context)
+ base_name = context[:base_name]
+ const_name = "#{base_name.upcase}_ENUM"
+ const_set(const_name, context[:enum]) unless const_defined?(const_name)
+ end
+
+ def enum_for(attr)
+ base_name = enum_base_name(attr)
+ name = "#{base_name.upcase}_ENUM"
+ const_defined?(name) ? const_get(name) : nil
+ end
+
+ def define_accessor(context)
+ attr = context[:attr]
+ return unless (instance_methods & [attr, "#{attr}="]).empty?
+ if context[:attr_accessor]
+ if context[:default]
+ if respond_to?(:attr_accessor_with_default)
+ attr_accessor_with_default(attr, context[:default])
+ else
+ instance_var_name = "@#{attr}"
+ attr_writer(attr)
+ define_method(attr) do
+ value = instance_variable_get(instance_var_name)
+ instance_variable_set(instance_var_name, value = context[:default]) unless value
+ value
+ end
+ end
+ else
+ attr_accessor(attr)
+ end
+ else
+ if context[:default]
+ SelectableAttr.logger.warn(":default option ignored for #{attr}")
+ end
+ end
+ end
+
+ def define_enum_class_methods(context)
+ base_name = context[:base_name]
+ enum = context[:enum]
+ mod = Module.new
+ mod.module_eval do
+ define_method("#{base_name}_enum"){enum}
+ define_method("#{base_name}_hash_array"){enum.to_hash_array}
+ define_method("#{base_name}_entries"){enum.entries}
+ define_method("#{base_name}_options"){|*ids_or_keys|enum.options(*ids_or_keys)}
+ define_method("#{base_name}_ids"){|*ids_or_keys| enum.ids(*ids_or_keys)}
+ define_method("#{base_name}_keys"){|*ids_or_keys|enum.keys(*ids_or_keys)}
+ define_method("#{base_name}_names"){|*ids_or_keys|enum.names(*ids_or_keys)}
+ define_method("#{base_name}_key_by_id"){|id|enum.key_by_id(id)}
+ define_method("#{base_name}_id_by_key"){|key|enum.id_by_key(key)}
+ define_method("#{base_name}_name_by_id"){|id|enum.name_by_id(id)}
+ define_method("#{base_name}_name_by_key"){|key|enum.name_by_key(key)}
+ define_method("#{base_name}_entry_by_id"){|id|enum.entry_by_id(id)}
+ define_method("#{base_name}_entry_by_key"){|key|enum.entry_by_key(key)}
+ end
+ if convertors = ENUM_ARRAY_METHODS[context[:convert_with] || :none]
+ mod.module_eval do
+ define_method("#{base_name}_to_hash_array", convertors[:to_hash_array])
+ define_method("hash_array_to_#{base_name}", convertors[:to_attr_value])
+ end
+ end
+ self.extend(mod)
+ end
+
+ def define_enum_instance_methods(context)
+ attr = context[:attr]
+ base_name = context[:base_name]
+ instance_methods = <<-EOS
+ def #{base_name}_key
+ self.class.#{base_name}_key_by_id(#{attr})
+ end
+ def #{base_name}_key=(key)
+ self.#{attr} = self.class.#{base_name}_id_by_key(key)
+ end
+ def #{base_name}_name
+ self.class.#{base_name}_name_by_id(#{attr})
+ end
+ def #{base_name}_entry
+ self.class.#{base_name}_entry_by_id(#{attr})
+ end
+ def #{base_name}_entry
+ self.class.#{base_name}_entry_by_id(#{attr})
+ end
+ EOS
+ self.module_eval(instance_methods)
+ end
+
+ def define_enum_array_instance_methods(context)
+ attr = context[:attr]
+ base_name = context[:base_name]
+ # ActiveRecord::Baseから継承している場合は、基本カラムに対応するメソッドはない
+ self.module_eval(<<-"EOS")
+ def #{base_name}_ids
+ #{base_name}_hash_array_selected.map{|hash|hash[:id]}
+ end
+ def #{base_name}_ids=(ids)
+ ids = ids.split(',') if ids.is_a?(String)
+ ids = ids ? ids.map(&:to_s) : []
+ update_#{base_name}_hash_array{|hash|ids.include?(hash[:id].to_s)}
+ end
+ def #{base_name}_hash_array
+ self.class.#{base_name}_to_hash_array(self.class.#{base_name}_enum, #{attr})
+ end
+ def #{base_name}_hash_array=(hash_array)
+ self.#{attr} = self.class.hash_array_to_#{base_name}(self.class.#{base_name}_enum, hash_array)
+ end
+ def #{base_name}_hash_array_selected
+ #{base_name}_hash_array.select{|hash|!!hash[:select]}
+ end
+ def update_#{base_name}_hash_array(&block)
+ hash_array = #{base_name}_hash_array.map do |hash|
+ hash.merge(:select => yield(hash))
+ end
+ self.#{base_name}_hash_array = hash_array
+ end
+ def #{base_name}_keys
+ #{base_name}_hash_array_selected.map{|hash|hash[:key]}
+ end
+ def #{base_name}_keys=(keys)
+ update_#{base_name}_hash_array{|hash|keys.include?(hash[:key])}
+ end
+ def #{base_name}_selection
+ #{base_name}_hash_array.map{|hash|!!hash[:select]}
+ end
+ def #{base_name}_selection=(selection)
+ idx = -1
+ update_#{base_name}_hash_array{|hash| idx += 1; !!selection[idx]}
+ end
+ def #{base_name}_names
+ #{base_name}_hash_array_selected.map{|hash|hash[:name]}
+ end
+ def #{base_name}_entries
+ ids = #{base_name}_ids
+ self.class.#{base_name}_enum.select{|entry|ids.include?(entry.id)}
+ end
+ EOS
+ end
+ end
+ end
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+module SelectableAttr
+
+ class Enum
+ include Enumerable
+
+ class << self
+ def instances
+ @@instances ||= []
+ end
+ end
+
+ def initialize(&block)
+ @entries = []
+ instance_eval(&block) if block_given?
+ SelectableAttr::Enum.instances << self
+ end
+
+ def entries
+ @entries
+ end
+
+ def each(&block)
+ entries.each(&block)
+ end
+
+ def define(id, key, name, options = nil, &block)
+ entry = Entry.new(self, id, key, name, options, &block)
+ entry.instance_variable_set(:@defined_in_code, true)
+ @entries << entry
+ end
+ alias_method :entry, :define
+
+ def i18n_scope(*path)
+ @i18n_scope = path unless path.empty?
+ @i18n_scope
+ end
+
+ def match_entry(entry, value, *attrs)
+ attrs.any?{|attr| entry[attr].to_s == value.to_s}
+ end
+
+ def entry_by(value, *attrs)
+ entries.detect{|entry| match_entry(entry, value, *attrs)} || Entry::NULL
+ end
+
+ def entry_by_id(id)
+ entry_by(id, :id)
+ end
+
+ def entry_by_key(key)
+ entry_by(key, :key)
+ end
+
+ def entry_by_id_or_key(id_or_key)
+ entry_by(id_or_key, :id, :key)
+ end
+
+ def entry_by_hash(attrs)
+ entries.detect{|entry| attrs.all?{|(attr, value)| entry[attr].to_s == value.to_s }} || Entry::NULL
+ end
+
+ def [](arg)
+ arg.is_a?(Hash) ? entry_by_hash(arg) : entry_by_id_or_key(arg)
+ end
+
+ def values(*args)
+ args = args.empty? ? [:name, :id] : args
+ result = entries.collect{|entry| args.collect{|arg| entry.send(arg) }}
+ (args.length == 1) ? result.flatten : result
+ end
+
+ def map_attrs(attrs, *ids_or_keys)
+ if attrs.is_a?(Array)
+ ids_or_keys.empty? ?
+ entries.map{|entry| attrs.map{|attr|entry.send(attr)}} :
+ ids_or_keys.map do |id_or_key|
+ entry = entry_by_id_or_key(id_or_key)
+ attrs.map{|attr|entry.send(attr)}
+ end
+ else
+ attr = attrs
+ ids_or_keys.empty? ?
+ entries.map(&attr.to_sym) :
+ ids_or_keys.map{|id_or_key|entry_by_id_or_key(id_or_key).send(attr)}
+ end
+ end
+
+ def ids(*ids_or_keys); map_attrs(:id, *ids_or_keys); end
+ def keys(*ids_or_keys); map_attrs(:key, *ids_or_keys); end
+ def names(*ids_or_keys); map_attrs(:name, *ids_or_keys); end
+ def options(*ids_or_keys); map_attrs([:name, :id], *ids_or_keys); end
+
+ def key_by_id(id); entry_by_id(id).key; end
+ def id_by_key(key); entry_by_key(key).id; end
+ def name_by_id(id); entry_by_id(id).name; end
+ def name_by_key(key); entry_by_key(key).name; end
+
+ def find(options = nil, &block)
+ entries.detect{|entry|
+ block_given? ? yield(entry) : entry.match?(options)
+ } || Entry::NULL
+ end
+
+ def to_hash_array
+ entries.map do |entry|
+ result = entry.to_hash
+ yield(result) if defined? yield
+ result
+ end
+ end
+
+ def length
+ entries.length
+ end
+ alias_method :size, :length
+
+ class Entry
+ BASE_ATTRS = [:id, :key, :name]
+ attr_reader :id, :key
+ attr_reader :defined_in_code
+ def initialize(enum, id, key, name, options = nil, &block)
+ @enum = enum
+ @id = id
+ @key = key
+ @name = name
+ @options = options
+ self.instance_eval(&block) if block
+ end
+
+ attr_reader :name
+
+ def [](option_key)
+ BASE_ATTRS.include?(option_key) ? send(option_key) :
+ @options ? @options[option_key] : nil
+ end
+
+ def match?(options)
+ @options === options
+ end
+
+ def null?
+ false
+ end
+
+ def null_object?
+ self.null?
+ end
+
+ def to_hash
+ (@options || {}).merge(:id => @id, :key => @key, :name => name)
+ end
+
+ def inspect
+ # object_idを2倍にしているのは通常のinspectと合わせるためです。
+ '#<%s:%x @id=%s, @key=%s, @name=%s, @options=%s>' % [
+ self.class.name, object_id * 2, id.inspect, key.inspect, name.inspect, @options.inspect]
+ end
+
+ NULL = new(nil, nil, nil, nil) do
+ def null?; true; end
+ def name; nil; end
+ end
+ end
+
+ end
+
+end
--- /dev/null
+module SelectableAttr
+ VERSION = '0.0.3'
+end
--- /dev/null
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{selectable_attr}
+ s.version = "0.3.7"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Takeshi Akima"]
+ s.date = %q{2009-08-17}
+ s.description = %q{selectable_attr generates extra methods dynamically for attribute which has options}
+ s.email = %q{akima@gmail.com}
+ s.extra_rdoc_files = [
+ "README"
+ ]
+ s.files = [
+ ".gitignore",
+ "MIT-LICENSE",
+ "README",
+ "Rakefile",
+ "VERSION.yml",
+ "init.rb",
+ "install.rb",
+ "lib/selectable_attr.rb",
+ "lib/selectable_attr/base.rb",
+ "lib/selectable_attr/enum.rb",
+ "lib/selectable_attr/version.rb",
+ "selectable_attr.gemspec",
+ "spec/selectable_attr_base_alias_spec.rb",
+ "spec/selectable_attr_enum_spec.rb",
+ "spec/spec_helper.rb",
+ "tasks/selectable_attr_tasks.rake",
+ "uninstall.rb"
+ ]
+ s.homepage = %q{http://github.com/akm/selectable_attr/}
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.4}
+ s.summary = %q{selectable_attr generates extra methods dynamically}
+ s.test_files = [
+ "spec/selectable_attr_base_alias_spec.rb",
+ "spec/selectable_attr_enum_spec.rb",
+ "spec/spec_helper.rb"
+ ]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ else
+ end
+ else
+ end
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+require File.join(File.dirname(__FILE__), 'spec_helper')
+require 'stringio'
+
+describe SelectableAttr do
+
+ def assert_enum_class_methods(klass, attr = :enum1)
+ klass.send("#{attr}_enum").length.should == 3
+ expected_hash_array = [
+ {:id => 1, :key => :entry1, :name => "エントリ1"},
+ {:id => 2, :key => :entry2, :name => "エントリ2"},
+ {:id => 3, :key => :entry3, :name => "エントリ3"}
+ ]
+ klass.send("#{attr}_hash_array").should == expected_hash_array
+ klass.send("#{attr}_enum").to_hash_array.should == expected_hash_array
+ klass.send("#{attr}_entries").
+ map{|entry| {:id => entry.id, :key => entry.key, :name => entry.name} }.
+ should == expected_hash_array
+
+ klass.send("#{attr}_ids").should == [1,2,3]
+ klass.send("#{attr}_ids", :entry2, :entry3).should == [2,3]
+ klass.send("#{attr}_keys").should == [:entry1, :entry2, :entry3]
+ klass.send("#{attr}_keys", 1,3).should == [:entry1, :entry3]
+ klass.send("#{attr}_names").should == ["エントリ1", "エントリ2", "エントリ3"]
+ klass.send("#{attr}_names", 1,2).should == ["エントリ1", "エントリ2"]
+ klass.send("#{attr}_names", :entry1, :entry2).should == ["エントリ1", "エントリ2"]
+ klass.send("#{attr}_options").should == [['エントリ1', 1], ['エントリ2', 2], ['エントリ3', 3]]
+ klass.send("#{attr}_options", :entry2, :entry3).should == [['エントリ2', 2], ['エントリ3', 3]]
+ klass.send("#{attr}_options", 1,2).should == [['エントリ1', 1], ['エントリ2', 2]]
+
+ klass.send("#{attr}_id_by_key", nil).should be_nil
+ klass.send("#{attr}_id_by_key", :entry1).should == 1
+ klass.send("#{attr}_id_by_key", :entry2).should == 2
+ klass.send("#{attr}_id_by_key", :entry3).should == 3
+ klass.send("#{attr}_name_by_key", nil).should be_nil
+ klass.send("#{attr}_name_by_key", :entry1).should == "エントリ1"
+ klass.send("#{attr}_name_by_key", :entry2).should == "エントリ2"
+ klass.send("#{attr}_name_by_key", :entry3).should == "エントリ3"
+
+ klass.send("#{attr}_key_by_id", nil).should be_nil
+ klass.send("#{attr}_key_by_id", 1).should == :entry1
+ klass.send("#{attr}_key_by_id", 2).should == :entry2
+ klass.send("#{attr}_key_by_id", 3).should == :entry3
+ klass.send("#{attr}_name_by_id", nil).should be_nil
+ klass.send("#{attr}_name_by_id", 1).should == "エントリ1"
+ klass.send("#{attr}_name_by_id", 2).should == "エントリ2"
+ klass.send("#{attr}_name_by_id", 3).should == "エントリ3"
+ end
+
+ def assert_single_enum_instance_methods(obj, attr = :enum1)
+ obj.send("#{attr}=", 1)
+ obj.send(attr).should == 1
+ obj.enum1_key.should == :entry1
+ obj.enum1_name.should == "エントリ1"
+ obj.enum1_entry.to_hash.should == {:id => 1, :key => :entry1, :name => "エントリ1"}
+
+ obj.enum1_key = :entry2
+ obj.send(attr).should == 2
+ obj.enum1_key.should == :entry2
+ obj.enum1_name.should == "エントリ2"
+ obj.enum1_entry.to_hash.should == {:id => 2, :key => :entry2, :name => "エントリ2"}
+
+ obj.send("#{attr}=", 3)
+ obj.send(attr).should == 3
+ obj.enum1_key.should == :entry3
+ obj.enum1_name.should == "エントリ3"
+ obj.enum1_entry.to_hash.should == {:id => 3, :key => :entry3, :name => "エントリ3"}
+ end
+
+ class EnumBase
+ include ::SelectableAttr::Base
+ end
+
+ describe "selectable_attr with default" do
+ class EnumMock1 < EnumBase
+ selectable_attr :enum1, :default => 2 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ class EnumMock1WithEnum < EnumBase
+ selectable_attr :enum1, :default => 2 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it "test_selectable_attr1" do
+ assert_enum_class_methods(EnumMock1)
+ mock1 = EnumMock1.new
+ mock1.enum1.should == 2
+ assert_single_enum_instance_methods(mock1)
+
+ assert_enum_class_methods(EnumMock1WithEnum)
+ mock1 = EnumMock1WithEnum.new
+ mock1.enum1.should == 2
+ assert_single_enum_instance_methods(mock1)
+
+ EnumMock1.selectable_attr_type_for(:enum1).should == :single
+ EnumMock1WithEnum.selectable_attr_type_for(:enum1).should == :single
+ end
+ end
+
+
+ describe "attr_enumeable_base" do
+ class EnumMock2 < EnumBase
+ attr_enumeable_base do |attr|
+ attr.to_s.gsub(/(.*)_code(.*)$/){"#{$1}#{$2}"}
+ end
+
+ selectable_attr :enum_code1 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ class EnumMock2WithEnum < EnumBase
+ attr_enumeable_base do |attr|
+ attr.to_s.gsub(/(.*)_code(.*)$/){"#{$1}#{$2}"}
+ end
+
+ enum :enum_code1 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it "test_selectable_attr2" do
+ assert_enum_class_methods(EnumMock2)
+ assert_single_enum_instance_methods(EnumMock2.new, :enum_code1)
+ assert_enum_class_methods(EnumMock2WithEnum)
+ assert_single_enum_instance_methods(EnumMock2WithEnum.new, :enum_code1)
+ end
+ end
+
+
+ def assert_multi_enum_instance_methods(obj, patterns)
+ obj.enum_array1_hash_array.should == [
+ {:id => 1, :key => :entry1, :name => "エントリ1", :select => false},
+ {:id => 2, :key => :entry2, :name => "エントリ2", :select => false},
+ {:id => 3, :key => :entry3, :name => "エントリ3", :select => false}
+ ]
+ obj.enum_array1_selection.should == [false, false, false]
+ obj.enum_array1.should be_nil
+ obj.enum_array1_entries.should == []
+ obj.enum_array1_keys.should == []
+ obj.enum_array1_names.should == []
+
+ obj.enum_array1 = patterns[0]
+ obj.enum_array1.should == patterns[0]
+ obj.enum_array1_hash_array.should == [
+ {:id => 1, :key => :entry1, :name => "エントリ1", :select => false},
+ {:id => 2, :key => :entry2, :name => "エントリ2", :select => false},
+ {:id => 3, :key => :entry3, :name => "エントリ3", :select => false}
+ ]
+ obj.enum_array1_selection.should == [false, false, false]
+ obj.enum_array1_entries.should == []
+ obj.enum_array1_keys.should == []
+ obj.enum_array1_names.should == []
+
+ obj.enum_array1 = patterns[1]
+ obj.enum_array1.should == patterns[1]
+ obj.enum_array1_hash_array.should == [
+ {:id => 1, :key => :entry1, :name => "エントリ1", :select => false},
+ {:id => 2, :key => :entry2, :name => "エントリ2", :select => false},
+ {:id => 3, :key => :entry3, :name => "エントリ3", :select => true}
+ ]
+ obj.enum_array1_selection.should == [false, false, true]
+ obj.enum_array1_entries.map(&:id).should == [3]
+ obj.enum_array1_keys.should == [:entry3]
+ obj.enum_array1_names.should == ['エントリ3']
+
+ obj.enum_array1 = patterns[3]
+ obj.enum_array1.should == patterns[3]
+ obj.enum_array1_hash_array.should == [
+ {:id => 1, :key => :entry1, :name => "エントリ1", :select => false},
+ {:id => 2, :key => :entry2, :name => "エントリ2", :select => true},
+ {:id => 3, :key => :entry3, :name => "エントリ3", :select => true}
+ ]
+ obj.enum_array1_selection.should == [false, true, true]
+ obj.enum_array1_entries.map(&:id).should == [2, 3]
+ obj.enum_array1_keys.should == [:entry2, :entry3]
+ obj.enum_array1_names.should == ['エントリ2', 'エントリ3']
+
+ obj.enum_array1 = patterns[7]
+ obj.enum_array1.should == patterns[7]
+ obj.enum_array1_hash_array.should == [
+ {:id => 1, :key => :entry1, :name => "エントリ1", :select => true},
+ {:id => 2, :key => :entry2, :name => "エントリ2", :select => true},
+ {:id => 3, :key => :entry3, :name => "エントリ3", :select => true}
+ ]
+ obj.enum_array1_selection.should == [true, true, true]
+ obj.enum_array1_ids.should == [1, 2, 3]
+ obj.enum_array1_entries.map(&:id).should == [1, 2, 3]
+ obj.enum_array1_keys.should == [:entry1, :entry2, :entry3]
+ obj.enum_array1_names.should == ['エントリ1', 'エントリ2', 'エントリ3']
+
+ obj.enum_array1_ids = [1,3]; obj.enum_array1.should == patterns[5]
+ obj.enum_array1_ids = [1,2]; obj.enum_array1.should == patterns[6]
+ obj.enum_array1_ids = [2]; obj.enum_array1.should == patterns[2]
+
+ obj.enum_array1_keys = [:entry1,:entry3]; obj.enum_array1.should == patterns[5]
+ obj.enum_array1_keys = [:entry1,:entry2]; obj.enum_array1.should == patterns[6]
+ obj.enum_array1_keys = [:entry2]; obj.enum_array1.should == patterns[2]
+
+ obj.enum_array1_selection = [true, false, true]; obj.enum_array1.should == patterns[5]
+ obj.enum_array1_selection = [true, true, false]; obj.enum_array1.should == patterns[6]
+ obj.enum_array1_selection = [false, true, false]; obj.enum_array1.should == patterns[2]
+
+ obj.enum_array1_ids = "1,3"; obj.enum_array1.should == patterns[5]
+ obj.enum_array1_ids = "1,2"; obj.enum_array1.should == patterns[6]
+ obj.enum_array1_ids = "2"; obj.enum_array1.should == patterns[2]
+ end
+
+ describe ":convert_with => :binary_string" do
+ class EnumMock3 < EnumBase
+ multi_selectable_attr :enum_array1, :convert_with => :binary_string do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ class EnumMock3WithEnumArray < EnumBase
+ enum_array :enum_array1, :convert_with => :binary_string do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it "test_multi_selectable_attr_with_binary_string" do
+ expected = (0..7).map{|i| '%-03b' % i} # ["000", "001", "010", "011", "100", "101", "110", "111"]
+ assert_enum_class_methods(EnumMock3, :enum_array1)
+ assert_multi_enum_instance_methods(EnumMock3.new, expected)
+ assert_enum_class_methods(EnumMock3WithEnumArray, :enum_array1)
+ assert_multi_enum_instance_methods(EnumMock3WithEnumArray.new, expected)
+ EnumMock3.selectable_attr_type_for(:enum_array1).should == :multi
+ end
+ end
+
+ describe "multi_selectable_attr" do
+ class EnumMock4 < EnumBase
+ multi_selectable_attr :enum_array1 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ class EnumMock4WithEnumArray < EnumBase
+ enum_array :enum_array1 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it "test_multi_selectable_attr2" do
+ # [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]
+ expected =
+ (0..7).map do |i|
+ s = '%03b' % i
+ a = s.split('').map{|v| v.to_i}
+ ret = []
+ a.each_with_index{|val, pos| ret << pos + 1 if val == 1}
+ ret
+ end
+ assert_enum_class_methods(EnumMock4, :enum_array1)
+ assert_multi_enum_instance_methods(EnumMock4.new, expected)
+ assert_enum_class_methods(EnumMock4WithEnumArray, :enum_array1)
+ assert_multi_enum_instance_methods(EnumMock4WithEnumArray.new, expected)
+ end
+ end
+
+ describe "convert_with" do
+ class EnumMock5 < EnumBase
+ multi_selectable_attr :enum_array1, :convert_with => :comma_string do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ class EnumMock5WithEnumArray < EnumBase
+ enum_array :enum_array1, :convert_with => :comma_string do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it "test_multi_selectable_attr_with_comma_string" do
+ # ["", "3", "2", "2,3", "1", "1,3", "1,2", "1,2,3"]
+ expected =
+ (0..7).map do |i|
+ s = '%03b' % i
+ a = s.split('').map{|v| v.to_i}
+ ret = []
+ a.each_with_index{|val, pos| ret << pos + 1 if val == 1}
+ ret.join(',')
+ end
+ assert_enum_class_methods(EnumMock5, :enum_array1)
+ assert_multi_enum_instance_methods(EnumMock5.new, expected)
+ assert_enum_class_methods(EnumMock5WithEnumArray, :enum_array1)
+ assert_multi_enum_instance_methods(EnumMock5WithEnumArray.new, expected)
+ end
+ end
+
+ describe "selectable_attr_name_pattern" do
+ class EnumMock6 < EnumBase
+ # self.selectable_attr_name_pattern = /(_cd$|_code$|_cds$|_codes$)/
+ selectable_attr :category_id do
+ entry "01", :category1, "カテゴリ1"
+ entry "02", :category2, "カテゴリ2"
+ end
+ end
+
+ class EnumMock7 < EnumBase
+ self.selectable_attr_name_pattern = /(_cd$|_id$|_cds$|_ids$)/
+ selectable_attr :category_id do
+ entry "01", :category1, "カテゴリ1"
+ entry "02", :category2, "カテゴリ2"
+ end
+ end
+
+ it "test_selectable_attr_name_pattern" do
+ EnumMock6.selectable_attr_name_pattern.should == /(_cd$|_code$|_cds$|_codes$)/
+ EnumMock6.respond_to?(:category_enum).should == false
+ EnumMock6.respond_to?(:category_id_enum).should == true
+ EnumMock6.new.respond_to?(:category_key).should == false
+ EnumMock6.new.respond_to?(:category_id_key).should == true
+
+ EnumMock7.selectable_attr_name_pattern.should == /(_cd$|_id$|_cds$|_ids$)/
+ EnumMock7.respond_to?(:category_enum).should == true
+ EnumMock7.respond_to?(:category_id_enum).should == false
+ EnumMock7.new.respond_to?(:category_key).should == true
+ EnumMock7.new.respond_to?(:category_id_key).should == false
+ end
+ end
+
+ describe "has_attr" do
+ class ConnectableMock1 < EnumBase
+ class << self
+ def columns; end
+ def connection; end
+ def connectec?; end
+ def table_exists?; end
+ end
+ end
+
+ it "should return false if column does exist" do
+ ConnectableMock1.should_receive(:connected?).and_return(true)
+ ConnectableMock1.should_receive(:table_exists?).and_return(true)
+ ConnectableMock1.should_receive(:columns).and_return([mock(:column1, :name => :column1)])
+ ConnectableMock1.has_attr(:column1).should == true
+ end
+
+ it "should return false if column doesn't exist" do
+ ConnectableMock1.should_receive(:connected?).and_return(true)
+ ConnectableMock1.should_receive(:table_exists?).and_return(true)
+ ConnectableMock1.should_receive(:columns).and_return([mock(:column1, :name => :column1)])
+ ConnectableMock1.has_attr(:unknown_column).should == false
+ end
+
+ it "should return nil unless connected" do
+ ConnectableMock1.should_receive(:connected?).and_return(false)
+ ConnectableMock1.should_receive(:table_exists?).and_return(false)
+ ConnectableMock1.has_attr(:unknown_column).should == false
+ end
+
+ it "should return nil if can't connection cause of Exception" do
+ ConnectableMock1.should_receive(:connected?).twice.and_return(false)
+ ConnectableMock1.should_receive(:connection).and_raise(IOError.new("can't connect to DB"))
+ ConnectableMock1.has_attr(:unknown_column).should == nil
+ end
+ end
+
+ describe "enum_for" do
+ class EnumMock10 < EnumBase
+ selectable_attr :enum1, :default => 2 do
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it "return constant by Symbol access" do
+ enum1 = EnumMock10.enum_for(:enum1)
+ enum1.class.should == SelectableAttr::Enum
+ end
+
+ it "return constant by String access" do
+ enum1 = EnumMock10.enum_for('enum1')
+ enum1.class.should == SelectableAttr::Enum
+ end
+
+ it "return nil for unexist attr" do
+ enum1 = EnumMock10.enum_for('unexist_attr')
+ enum1.should == nil
+ end
+
+ end
+
+ describe "define_accessor" do
+ class DefiningMock1 < EnumBase
+ class << self
+ def attr_accessor_with_default(*args); end
+ end
+ end
+
+ it "should call attr_accessor_with_default when both of attr_accessor and default are given" do
+ DefiningMock1.should_receive(:attr_accessor_with_default).with(:enum1, 1)
+ DefiningMock1.define_accessor(:attr => :enum1, :attr_accessor => true, :default => 1)
+ end
+
+ it "should call attr_accessor_with_default when default are given but attr_accessor is not TRUE" do
+ SelectableAttr.logger.should_receive(:warn).with(":default option ignored for enum1")
+ DefiningMock1.define_accessor(:attr => :enum1, :attr_accessor => false, :default => 1)
+ end
+
+ it "warning message to logger" do
+ io = StringIO.new
+ SelectableAttr.logger = Logger.new(io)
+ DefiningMock1.define_accessor(:attr => :enum1, :attr_accessor => false, :default => 1)
+ io.rewind
+ io.read.should =~ /WARN -- : :default option ignored for enum1$/
+ end
+
+
+ end
+
+
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+require File.join(File.dirname(__FILE__), 'spec_helper')
+
+describe SelectableAttr::Enum do
+
+ Enum1 = SelectableAttr::Enum.new do
+ entry 1, :book, '書籍'
+ entry 2, :dvd, 'DVD'
+ entry 3, :cd, 'CD'
+ entry 4, :vhs, 'VHS'
+ end
+
+ it "test_define" do
+ Enum1[1].id.should == 1
+ Enum1[2].id.should == 2
+ Enum1[3].id.should == 3
+ Enum1[4].id.should == 4
+ Enum1[1].key.should == :book
+ Enum1[2].key.should == :dvd
+ Enum1[3].key.should == :cd
+ Enum1[4].key.should == :vhs
+ Enum1[1].name.should == '書籍'
+ Enum1[2].name.should == 'DVD'
+ Enum1[3].name.should == 'CD'
+ Enum1[4].name.should == 'VHS'
+
+ Enum1[:book].id.should == 1
+ Enum1[:dvd ].id.should == 2
+ Enum1[:cd ].id.should == 3
+ Enum1[:vhs ].id.should == 4
+ Enum1[:book].key.should == :book
+ Enum1[:dvd ].key.should == :dvd
+ Enum1[:cd ].key.should == :cd
+ Enum1[:vhs ].key.should == :vhs
+ Enum1[:book].name.should == '書籍'
+ Enum1[:dvd].name.should == 'DVD'
+ Enum1[:cd].name.should == 'CD'
+ Enum1[:vhs].name.should == 'VHS'
+
+ Enum1.values.should == [['書籍', 1], ['DVD', 2], ['CD', 3], ['VHS', 4]]
+ Enum1.values(:name, :id).should == [['書籍', 1], ['DVD', 2], ['CD', 3], ['VHS', 4]]
+ Enum1.values(:name, :key).should == [['書籍', :book], ['DVD', :dvd], ['CD', :cd], ['VHS', :vhs]]
+ end
+
+ InetAccess = SelectableAttr::Enum.new do
+ entry 1, :email, 'Eメール', :protocol => 'mailto:'
+ entry 2, :website, 'ウェブサイト', :protocol => 'http://'
+ entry 3, :ftp, 'FTP', :protocol => 'ftp://'
+ end
+
+ it "test_define_with_options" do
+ InetAccess[1].id.should == 1
+ InetAccess[2].id.should == 2
+ InetAccess[3].id.should == 3
+ InetAccess[1].key.should == :email
+ InetAccess[2].key.should == :website
+ InetAccess[3].key.should == :ftp
+
+
+ InetAccess[1].name.should == 'Eメール'
+ InetAccess[2].name.should == 'ウェブサイト'
+ InetAccess[3].name.should == 'FTP'
+ InetAccess[1][:protocol].should == 'mailto:'
+ InetAccess[2][:protocol].should == 'http://'
+ InetAccess[3][:protocol].should == 'ftp://'
+
+ InetAccess[9].id.should be_nil
+ InetAccess[9].key.should be_nil
+ InetAccess[9].name.should be_nil
+ InetAccess[9][:protocol].should be_nil
+ InetAccess[9][:xxxx].should be_nil
+ end
+
+ it "test_get_by_option" do
+ InetAccess[:protocol => 'mailto:'].should == InetAccess[1]
+ InetAccess[:protocol => 'http://'].should == InetAccess[2]
+ InetAccess[:protocol => 'ftp://'].should == InetAccess[3]
+ end
+
+ it "test_null?" do
+ InetAccess[1].null?.should == false
+ InetAccess[2].null?.should == false
+ InetAccess[3].null?.should == false
+ InetAccess[9].null?.should == true
+ InetAccess[:protocol => 'mailto:'].null?.should == false
+ InetAccess[:protocol => 'http://'].null?.should == false
+ InetAccess[:protocol => 'ftp://'].null?.should == false
+ InetAccess[:protocol => 'svn://'].null?.should == true
+ end
+
+ it "test_null_object?" do
+ InetAccess[1].null_object?.should == false
+ InetAccess[2].null_object?.should == false
+ InetAccess[3].null_object?.should == false
+ InetAccess[9].null_object?.should == true
+ InetAccess[:protocol => 'mailto:'].null_object?.should == false
+ InetAccess[:protocol => 'http://'].null_object?.should == false
+ InetAccess[:protocol => 'ftp://'].null_object?.should == false
+ InetAccess[:protocol => 'svn://'].null_object?.should == true
+ end
+
+ it "test_to_hash_array" do
+ Enum1.to_hash_array.should == [
+ {:id => 1, :key => :book, :name => '書籍'},
+ {:id => 2, :key => :dvd, :name => 'DVD'},
+ {:id => 3, :key => :cd, :name => 'CD'},
+ {:id => 4, :key => :vhs, :name => 'VHS'}
+ ]
+
+ InetAccess.to_hash_array.should == [
+ {:id => 1, :key => :email, :name => 'Eメール', :protocol => 'mailto:'},
+ {:id => 2, :key => :website, :name => 'ウェブサイト', :protocol => 'http://'},
+ {:id => 3, :key => :ftp, :name => 'FTP', :protocol => 'ftp://'}
+ ]
+ end
+
+ describe "find" do
+ it "with options" do
+ InetAccess.find(:protocol => 'http://').should == InetAccess[2]
+ InetAccess.find(:protocol => 'svn+ssh://').should == SelectableAttr::Enum::Entry::NULL
+ end
+
+ it "with block" do
+ InetAccess.find{|entry| entry.key.to_s =~ /tp/}.should == InetAccess[3]
+ InetAccess.find{|entry| entry.key.to_s =~ /XXXXXX/}.should == SelectableAttr::Enum::Entry::NULL
+ end
+ end
+
+ describe :inspect do
+ it "NULL" do
+ SelectableAttr::Enum::Entry::NULL.inspect.should =~ /\#<SelectableAttr::Enum::Entry:[0-9a-f]+\ @id=nil,\ @key=nil,\ @name=nil,\ @options=nil>/
+ end
+
+ it "valid" do
+ InetAccess[1].inspect.should =~ /\#<SelectableAttr::Enum::Entry:[0-9a-f]+\ @id=1,\ @key=:email,\ @name="Eメール",\ @options=\{:protocol=>"mailto:"\}>/
+ end
+ end
+
+end
--- /dev/null
+$KCODE='u'
+
+$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
+require File.join(File.dirname(__FILE__), '..', 'init')
+
+def assert_hash(expected, actual)
+ keys = (expected.keys + actual.keys).uniq
+ keys.each do |key|
+ assert_equal expected[key], actual[key], "unmatch value for #{key.inspect}"
+ end
+end
--- /dev/null
+require 'yaml'
+require 'yaml_waml'
+
+namespace :i18n do
+ namespace :selectable_attr do
+ task :load_all_models => :environment do
+ Dir.glob(File.join(RAILS_ROOT, 'app', 'models', '**', '*.rb')) do |file_name|
+ require file_name
+ end
+ end
+
+ desc "Export i18n resources for selectable_attr entries"
+ task :export => :"i18n:selectable_attr:load_all_models" do
+ obj = {I18n.locale => SelectableAttr::Enum.i18n_export}
+ puts YAML.dump(obj)
+ end
+ end
+end
--- /dev/null
+# Uninstall hook code here
--- /dev/null
+Copyright (c) 2008 Takeshi AKIMA
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+= SelectableAttrRails
+
+== Introduction
+selectable_attr_railsは、selectable_attrをRailsで使うときに便利なヘルパーメソッドを提供し、
+エントリをDBから取得したり、I18n対応するものです。
+http://github.com/akm/selectable_attr_rails/tree/master
+
+selectable_attr は、コードが割り振られるような特定の属性について*コード*、*プログラム上での名前*、
+*表示するための名前*などをまとめて管理するものです。
+http://github.com/akm/selectable_attr/tree/master
+
+
+== Install
+=== a. plugin install
+ ruby script/plugin install git://github.com/akm/selectable_attr.git
+ ruby script/plugin install git://github.com/akm/selectable_attr_rails.git
+
+=== b. gem install
+ [sudo] gem install selectable_attr_rails
+
+config/initializers/selectable_attr.rb
+ require 'selectable_attr'
+ require 'selectable_attr_i18n'
+ require 'selectable_attr_rails'
+ SelectableAttrRails.setup
+
+
+== チュートリアル
+
+=== selectヘルパーメソッド
+以下のようなモデルが定義してあった場合
+ class Person < ActiveRecord::Base
+ include ::SelectableAttr::Base
+
+ selectable_attr :gender do
+ entry '1', :male, '男性'
+ entry '2', :female, '女性'
+ entry '9', :other, 'その他'
+ end
+ end
+
+ビューでは以下のように選択肢を表示することができます。
+ <% form_for(:person) do |f| %>
+ <%= f.select :gender %>
+ <% end %>
+
+form_for、fields_forを使用しない場合でも、オブジェクト名を設定して使用可能です。
+ <%= select :person, :gender %>
+
+また以下のように複数の値を取りうる場合にもこのメソドを使用することが可能です。
+ class RoomSearch
+ include ::SelectableAttr::Base
+
+ multi_selectable_attr :room_type do
+ entry '01', :single, 'シングル'
+ entry '02', :twin, 'ツイン'
+ entry '03', :double, 'ダブル'
+ entry '04', :triple, 'トリプル'
+ end
+ end
+
+ <% form_for(:room_search) do |f| %>
+ <%= f.select :room_type %>
+ <% end %>
+
+この場合、出力されるselectタグのmultiple属性が設定されます。
+
+
+
+=== radio_button_groupヘルパーメソッド
+ 一つだけ値を選択するUIの場合、selectメソッドではなく<input type="radio".../>を出力することも可能です。
+ 上記Personモデルの場合
+
+ <% form_for(:person) do |f| %>
+ <%= f.radio_button_group :gender %>
+ <% end %>
+
+ この場合、<input type="radio" .../><label for="xxx">... という風に続けて出力されるので、改行などを出力したい場合は
+ 引数を一つ取るブロックを渡して以下のように記述します。
+
+ <% form_for(:person) do |f| %>
+ <% f.radio_button_group :gender do |b| %>
+ <% b.each do %>
+ <%= b.radio_button %>
+ <%= b.label %>
+ <br/>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ f.radio_button_groupを呼び出しているERBのタグが、<%= %>から<% %>に変わっていることにご注意ください。
+
+
+=== check_box_groupヘルパーメソッド
+ 複数の値を選択するUIの場合、selectメソッドではなく<input type="checkbox".../>を出力することも可能です。
+ 上記RoomSearchクラスの場合
+
+ <% form_for(:room_search) do |f| %>
+ <%= f.check_box_group :room_type %>
+ <% end %>
+
+ この場合、<input type="checkbox" .../><label for="xxx">... という風に続けて出力されるので、改行などを出力したい場合は
+ 引数を一つ取るブロックを渡して以下のように記述します。
+
+ <% form_for(:person) do |f| %>
+ <% f.check_box_group :gender do |b| %>
+ <% b.each do %>
+ <%= b.check_box %>
+ <%= b.label %>
+ <br/>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ f.check_box_groupを呼び出しているERBのタグが、<%= %>から<% %>に変わっていることにご注意ください。
+
+
+== DBからのエントリの更新/追加
+各エントリの名称を実行時に変更したり、項目を追加することが可能です。
+
+ class RoomPlan < ActiveRecord::Base
+ include ::SelectableAttr::Base
+
+ selectable_attr :room_type do
+ update_by "select room_type, name from room_types"
+ entry '01', :single, 'シングル'
+ entry '02', :twin, 'ツイン'
+ entry '03', :double, 'ダブル'
+ entry '04', :triple, 'トリプル'
+ end
+ end
+
+というモデルと
+
+ create_table "room_types" do |t|
+ t.string "room_type", :limit => 2
+ t.string "name", :limit => 20
+ end
+
+というマイグレーションで作成されるテーブルがあったとします。
+
+=== エントリの追加
+room_typeが"05"、nameが"4ベッド"というレコードがINSERTされた後、
+RoomPlan#room_type_optionsなどのselectable_attrが提供するメソッドで
+各エントリへアクセスすると、update_byで指定されたSELECT文が実行され、
+エントリとしては、
+ entry '05', :entry_05, '4ベッド'
+が定義されている状態と同じようになります。
+
+このようにコードで定義されていないエントリは、DELETEされると、エントリもなくなります。
+
+=== エントリの名称の更新
+実行時に名称を変えたい場合には、そのidに該当するレコードを追加/更新します。
+例えば、
+room_typeが"04"、nameが"3ベッド"というレコードがINSERTされると、その後は
+04のエントリはの名称は"3ベッド"に変わり、また別の名称にUPDATEすると、それに
+よってエントリの名称も変わります。
+
+このようにコードによってエントリが定義されている場合は、DELETEされてもエントリは削除されず、
+DELETE後は、名称が元に戻ります。
+
+
+== I18n対応
+エントリのロケールにおける名称をRails2.2からの機能である、I18nを内部的にしようして取得できます。
+
+上記RoomPlanモデルの場合、
+
+config/locales/ja.yml
+ja:
+ selectable_attrs:
+ room_types:
+ single: シングル
+ twin: ツイン
+ double: ダブル
+ triple: トリプル
+
+config/locales/en.yml
+en:
+ selectable_attrs:
+ room_types:
+ single: Single
+ twin: Twin
+ double: Double
+ triple: Triple
+
+というYAMLを用意した上で、モデルを以下のように記述します。
+
+ class RoomPlan < ActiveRecord::Base
+ include ::SelectableAttr::Base
+
+ selectable_attr :room_type do
+ i18n_scope(:selectable_attrs, :room_types)
+ entry '01', :single, 'シングル'
+ entry '02', :twin, 'ツイン'
+ entry '03', :double, 'ダブル'
+ entry '04', :triple, 'トリプル'
+ end
+ end
+
+これで、I18n.localeに設定されているロケールに従って各エントリの名称が変わります。
+
+
+== Credit
+Copyright (c) 2008 Takeshi AKIMA, released under the MIT lice nse
--- /dev/null
+require 'rubygems'
+gem 'rspec', '>= 1.1.4'
+require 'rake'
+require 'rake/rdoctask'
+require 'spec/rake/spectask'
+require 'spec/rake/verify_rcov'
+
+desc 'Default: run unit tests.'
+task :default => :spec
+
+task :pre_commit => [:spec, 'coverage:verify']
+
+desc 'Run all specs under spec/**/*_spec.rb'
+Spec::Rake::SpecTask.new(:spec => 'coverage:clean') do |t|
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ t.spec_opts = ["-c", "--diff"]
+ t.rcov = true
+ t.rcov_opts = ["--include-file", "lib\/*\.rb", "--exclude", "spec\/"]
+end
+
+desc 'Generate documentation for the selectable_attr_rails plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'SelectableAttrRails'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+namespace :coverage do
+ desc "Delete aggregate coverage data."
+ task(:clean) { rm_f "coverage" }
+
+ desc "verify coverage threshold via RCov"
+ RCov::VerifyTask.new(:verify => :spec) do |t|
+ t.threshold = 100.0 # Make sure you have rcov 0.7 or higher!
+ t.index_html = 'coverage/index.html'
+ end
+end
+
+begin
+ require 'jeweler'
+ Jeweler::Tasks.new do |s|
+ s.name = "selectable_attr_rails"
+ s.summary = "selectable_attr_rails makes possible to use selectable_attr in rails application"
+ s.description = "selectable_attr_rails makes possible to use selectable_attr in rails application"
+ s.email = "akima@gmail.com"
+ s.homepage = "http://github.com/akm/selectable_attr_rails/"
+ s.authors = ["Takeshi Akima"]
+ s.add_dependency("activesupport", ">= 2.0.2")
+ s.add_dependency("activerecord", ">= 2.0.2")
+ s.add_dependency("actionpack", ">= 2.0.2")
+ s.add_dependency("selectable_attr", ">= 0.3.11")
+ end
+rescue LoadError
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
+end
--- /dev/null
+---
+:patch: 11
+:build:
+:major: 0
+:minor: 3
--- /dev/null
+require 'selectable_attr'
+require 'selectable_attr_i18n'
+require 'selectable_attr_rails'
+SelectableAttrRails.setup
--- /dev/null
+# Install hook code here
--- /dev/null
+if defined?(I18n)
+ require 'selectable_attr'
+
+ module SelectableAttr
+ class Enum
+ def self.i18n_export(enums = nil)
+ enums ||= instances
+ result = {}
+ enums.each do |instance|
+ unless instance.i18n_scope
+ SelectableAttrRails.logger.debug("no i18n_scope of #{instance.inspect}")
+ next
+ end
+ paths = instance.i18n_scope.dup
+ current = result
+ paths.each do |path|
+ current = current[path.to_s] ||= {}
+ end
+ instance.entries.each do |entry|
+ current[entry.key.to_s] = entry.name
+ end
+ end
+ result
+ end
+
+ def i18n_scope(*path)
+ @i18n_scope = path unless path.empty?
+ @i18n_scope
+ end
+
+ class Entry
+ def name
+ I18n.locale.nil? ? @name :
+ @enum.i18n_scope.blank? ? @name :
+ I18n.translate(key, :scope => @enum.i18n_scope, :default => @name)
+ end
+ end
+
+ end
+ end
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+require 'selectable_attr'
+
+module SelectableAttrRails
+ autoload :Helpers, 'selectable_attr_rails/helpers'
+ autoload :DbLoadable, 'selectable_attr_rails/db_loadable'
+ autoload :Validatable, 'selectable_attr_rails/validatable'
+
+ class << self
+ def logger
+ @logger ||= Logger.new(STDERR)
+ end
+ def logger=(value)
+ @logger = value
+ end
+
+ def add_features_to_active_record
+ ActiveRecord::Base.module_eval do
+ include ::SelectableAttr::Base
+ include ::SelectableAttrRails::Validatable::Base
+ end
+ SelectableAttr::Enum.module_eval do
+ include ::SelectableAttrRails::DbLoadable
+ include ::SelectableAttrRails::Validatable::Enum
+ end
+ logger.debug("#{self.name}.add_features_to_active_record")
+ end
+
+ def add_features_to_action_view
+ ActionView::Base.module_eval do
+ include ::SelectableAttrRails::Helpers::SelectHelper::Base
+ include ::SelectableAttrRails::Helpers::CheckBoxGroupHelper::Base
+ include ::SelectableAttrRails::Helpers::RadioButtonGroupHelper::Base
+ end
+ ActionView::Helpers::FormBuilder.module_eval do
+ include ::SelectableAttrRails::Helpers::SelectHelper::FormBuilder
+ include ::SelectableAttrRails::Helpers::CheckBoxGroupHelper::FormBuilder
+ include ::SelectableAttrRails::Helpers::RadioButtonGroupHelper::FormBuilder
+ end
+ logger.debug("#{self.name}.add_features_to_action_view")
+ end
+
+ def add_features_to_rails
+ add_features_to_active_record
+ add_features_to_action_view
+ end
+
+ def setup
+ logger = defined?(Rails) ? Rails.logger : ActiveRecord::Base.logger
+ self.logger = logger
+ SelectableAttr.logger = logger if SelectableAttr.respond_to?(:logger=) # since 0.3.11
+ add_features_to_rails
+ end
+ end
+
+end
--- /dev/null
+module SelectableAttrRails
+ module DbLoadable
+ def update_by(*args, &block)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ options = {:when => :first_time}.update(options)
+ @sql_to_update = block_given? ? block : args.first
+ @update_timing = options[:when]
+ self.extend(InstanceMethods) unless respond_to?(:update_entries)
+ end
+
+ module Entry
+
+ if defined?(I18n)
+ def name_from_db
+ @names_from_db ||= {}
+ @names_from_db[I18n.locale.to_s]
+ end
+
+ def name_from_db=(value)
+ @names_from_db ||= {}
+ @names_from_db[I18n.locale.to_s] = value
+ end
+
+ def name_with_from_db
+ name_from_db || name_without_from_db
+ end
+
+ else
+
+ attr_accessor :name_from_db
+ def name_with_from_db
+ @name_from_db || name_without_from_db
+ end
+
+ end
+
+ def self.extended(obj)
+ obj.instance_eval do
+ alias :name_without_from_db :name
+ alias :name :name_with_from_db
+ end
+ end
+
+ end
+
+ module InstanceMethods
+ def entries
+ update_entries if must_be_updated?
+ @entries
+ end
+
+ def must_be_updated?
+ return false if @update_timing == :never
+ return true if @update_timing == :everytime
+ end
+
+ def update_entries
+ unless @original_entries
+ @original_entries = @entries.dup
+ @original_entries.each do |entry|
+ entry.extend(SelectableAttrRails::DbLoadable::Entry) unless respond_to?(:name_from_db)
+ end
+ end
+ records = nil
+ if @sql_to_update.respond_to?(:call)
+ records = @sql_to_update.call
+ else
+ sql = @sql_to_update.gsub(/\:locale/, I18n.locale.to_s.inspect)
+ records = ActiveRecord::Base.connection.select_rows(sql)
+ end
+
+ new_entries = []
+ records.each do |r|
+ if entry = @original_entries.detect{|entry| entry.id == r.first}
+ entry.name_from_db = r.last unless r.last.blank?
+ new_entries << entry
+ else
+ entry = SelectableAttr::Enum::Entry.new(self, r.first, "entry_#{r.first}".to_sym, r.last)
+ entry.extend(SelectableAttrRails::DbLoadable::Entry)
+ entry.name_from_db = r.last
+ new_entries << entry
+ end
+ end
+ @original_entries.each do |entry|
+ unless new_entries.include?(entry)
+ entry.name_from_db = nil
+ new_entries << entry if entry.defined_in_code
+ end
+ end
+ @entries = new_entries
+ end
+ end
+
+ end
+end
--- /dev/null
+module SelectableAttrRails
+ module Helpers
+ autoload :SelectHelper, 'selectable_attr_rails/helpers/select_helper'
+ autoload :CheckBoxGroupHelper, 'selectable_attr_rails/helpers/check_box_group_helper'
+ autoload :RadioButtonGroupHelper, 'selectable_attr_rails/helpers/radio_button_group_helper'
+ end
+end
--- /dev/null
+module SelectableAttrRails::Helpers
+ class AbstractSelectionBuilder
+ attr_reader :entry_hash
+
+ def initialize(object, object_name, method, options, template)
+ @object, @object_name, @method = object, object_name, method
+ @base_name = @object.class.enum_base_name(method.to_s)
+ @template = template
+ @entry_hash = nil
+ @options = options || {}
+ @entry_hash_array = @options[:entry_hash_array]
+ end
+
+ def enum_hash_array_from_object
+ base_name = @object.class.enum_base_name(@method.to_s)
+ @object.send("#{base_name}_hash_array")
+ end
+
+ def enum_hash_array_from_class
+ base_name = @object.class.enum_base_name(@method.to_s)
+ @object.class.send("#{base_name}_hash_array")
+ end
+
+ def tag_id(tag)
+ result = nil
+ tag.scan(/ id\=\"(.*?)\"/){|s|result = s}
+ return result
+ end
+
+ def add_class_name(options, class_name)
+ (options ||= {}).stringify_keys!
+ (options['class'] ||= '') << ' ' << class_name
+ options
+ end
+
+ def camelize_keys(hash, first_letter = :lower)
+ result = {}
+ hash.each{|key, value|result[key.to_s.camelize(first_letter)] = value}
+ result
+ end
+
+ def update_options(dest, *options_array)
+ result = dest || {}
+ options_array.each do |options|
+ next unless options
+ if class_name = options.delete(:class)
+ add_class_name(result, class_name)
+ end
+ result.update(options)
+ end
+ result
+ end
+
+ end
+end
--- /dev/null
+require 'selectable_attr_rails/helpers/abstract_selection_helper'
+module SelectableAttrRails::Helpers
+ module CheckBoxGroupHelper
+ class Builder < SelectableAttrRails::Helpers::AbstractSelectionBuilder
+
+ def initialize(object, object_name, method, options, template)
+ super(object, object_name, method, options, template)
+ @entry_hash_array ||= enum_hash_array_from_object
+ @param_name = "#{@base_name}_ids"
+ @check_box_options = @options.delete(:check_box) || {}
+ end
+
+ def each(&block)
+ @entry_hash_array.each do |@entry_hash|
+ @tag_value = @entry_hash[:id].to_s.gsub(/\s/, "_").gsub(/\W/, "")
+ @check_box_id = "#{@object_name}_#{@param_name}_#{@tag_value}"
+ yield(self)
+ end
+ end
+
+ def check_box(options = nil)
+ options = update_options({
+ :id => @check_box_id, :type => 'checkbox', :value => @tag_value,
+ :name => "#{@object_name}[#{@param_name}][]"
+ }, @check_box_options, options)
+ options[:checked] = 'checked' if @entry_hash[:select]
+ @template.content_tag("input", nil, options)
+ end
+
+ def label(text = nil, options = nil)
+ @template.content_tag("label", text || @entry_hash[:name],
+ update_options({:for => @check_box_id}, options))
+ end
+ end
+
+ module Base
+ def check_box_group(object_name, method, options = nil, &block)
+ object = (options || {})[:object] || instance_variable_get("@#{object_name}")
+ builder = Builder.new(object, object_name, method, options, @template)
+ if block_given?
+ yield(builder)
+ return nil
+ else
+ result = ''
+ builder.each do
+ result << builder.check_box
+ result << ' '
+ result << builder.label
+ result << ' '
+ end
+ return result
+ end
+ end
+ end
+
+ module FormBuilder
+ def check_box_group(method, options = nil, &block)
+ @template.check_box_group(@object_name, method,
+ (options || {}).merge(:object => @object), &block)
+ end
+ end
+ end
+end
--- /dev/null
+module SelectableAttrRails::Helpers
+ module RadioButtonGroupHelper
+ class Builder < SelectableAttrRails::Helpers::AbstractSelectionBuilder
+
+ attr_reader :entry_hash_array
+ attr_reader :entry_hash
+ attr_accessor :radio_button_id
+
+ def initialize(object, object_name, method, options, template)
+ super(object, object_name, method, options, template)
+ @entry_hash_array ||= enum_hash_array_from_class
+ end
+
+ def each(&block)
+ @entry_hash_array.each do |entry_hash|
+ @entry_hash = entry_hash
+ tag_value = @entry_hash[:id].to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
+ @radio_button_id = "#{@object_name}_#{@method}_#{tag_value}"
+ yield(self)
+ end
+ end
+
+ def radio_button(options = nil)
+ @template.radio_button(@object_name, @method, @entry_hash[:id],
+ update_options({:id => @radio_button_id}, options))
+ end
+
+ def label(text = nil, options = nil)
+ @template.content_tag("label", text || @entry_hash[:name],
+ update_options({:for => @radio_button_id}, options))
+ end
+ end
+
+ module Base
+ def radio_button_group(object_name, method, options = nil, &block)
+ object = (options || {})[:object] || instance_variable_get("@#{object_name}")
+ builder = Builder.new(object, object_name, method, options, self)
+ if block_given?
+ yield(builder)
+ return nil
+ else
+ result = ''
+ builder.each do
+ result << builder.radio_button
+ result << builder.label
+ end
+ return result
+ end
+ end
+ end
+
+ module FormBuilder
+ def radio_button_group(method, options = nil, &block)
+ @template.radio_button_group(@object_name, method,
+ (options || {}).merge(:object => @object), &block)
+ end
+ end
+ end
+end
--- /dev/null
+module SelectableAttrRails::Helpers
+ module SelectHelper
+ module Base
+ def self.included(base)
+ base.module_eval do
+ alias_method_chain :select, :attr_enumeable
+ end
+ end
+
+ # def select_with_attr_enumeable(object, method, choices, options = {}, html_options = {})
+ def select_with_attr_enumeable(object_name, method, *args, &block)
+ if args.length > 3
+ raise ArgumentError, "argument must be " <<
+ "(object, method, choices, options = {}, html_options = {}) or " <<
+ "(object, method, options = {}, html_options = {})"
+ end
+ return select_without_attr_enumeable(object_name, method, *args, &block) if args.length == 3
+ return select_without_attr_enumeable(object_name, method, *args, &block) if args.first.is_a?(Array)
+ options, html_options = *args
+ options = update_enum_select_options(options, object_name, method)
+ object, base_name = options[:object], options[:base_name]
+ return multi_enum_select(object_name, method, options, html_options, &block) if object.respond_to?("#{base_name}_hash_array")
+ return single_enum_select(object_name, method, options, html_options, &block) if object.class.respond_to?("#{base_name}_hash_array")
+ raise ArgumentError, "invaliad argument"
+ end
+
+ def single_enum_select(object_name, method, options = {}, html_options = {}, &block)
+ options = update_enum_select_options(options, object_name, method)
+ object = options[:object] # options.delete(:object)
+ base_name = options.delete(:base_name)
+ entry_hash_array = options.delete(:entry_hash_array) || object.class.send("#{base_name}_hash_array")
+ container = entry_hash_array.map{|hash| [hash[:name].to_s, hash[:id]]}
+ select_without_attr_enumeable(object_name, method, container, options, html_options || {}, &block)
+ end
+
+ def multi_enum_select(object_name, method, options = {}, html_options = {}, &block)
+ html_options = {:size => 5, :multiple => 'multiple'}.update(html_options || {})
+ options = update_enum_select_options(options, object_name, method)
+ object = options.delete(:object)
+ base_name = options.delete(:base_name)
+ entry_hash_array = options.delete(:entry_hash_array) || object.send("#{base_name}_hash_array")
+ container = entry_hash_array.map{|hash| [hash[:name].to_s, hash[:id].to_s]}
+ attr = "#{base_name}_ids"
+ select_without_attr_enumeable(object_name, attr, container, options, html_options, &block)
+ end
+
+ def update_enum_select_options(options, object_name, method)
+ options ||= {}
+ object = (options[:object] ||= instance_variable_get("@#{object_name}"))
+ options[:base_name] ||= object.class.enum_base_name(method.to_s)
+ options
+ end
+ end
+
+ module FormBuilder
+ def self.included(base)
+ base.module_eval do
+ alias_method_chain :select, :attr_enumeable
+ end
+ end
+
+ # def select_with_attr_enumeable(method, choices, options = {}, html_options = {}, &block)
+ def select_with_attr_enumeable(method, *args, &block)
+ options = args.first.is_a?(Array) ? (args[1] ||= {}) : (args[0] ||= {})
+ object = (options || {}).delete(:object) || @object || instance_variable_get(@object_name) rescue nil
+ options.update({:object => object})
+ options[:selected] = object.send(method) if object
+ @template.select(@object_name, method, *args, &block)
+ end
+ end
+ end
+end
--- /dev/null
+require 'selectable_attr_rails'
+
+module SelectableAttrRails
+ module Validatable
+ autoload :Base, 'selectable_attr_rails/validatable/base'
+ autoload :Enum, 'selectable_attr_rails/validatable/enum'
+ end
+end
--- /dev/null
+require 'selectable_attr_rails/validatable'
+
+module SelectableAttrRails
+ module Validatable
+ module Base
+ def self.included(mod)
+ mod.extend(ClassMethods)
+ mod.instance_eval do
+ alias :define_enum_without_validatable :define_enum
+ alias :define_enum :define_enum_with_validatable
+ end
+ end
+
+ module ClassMethods
+ def define_enum_with_validatable(context)
+ enum = context[:enum]
+ if options = enum.validates_format_options
+ options[:with] = Regexp.union(*enum.entries.map{|entry| /#{Regexp.escape(entry.id)}/})
+ entry_format = options.delete(:entry_format) || '#{entry.name}'
+ entries = enum.entries.map{|entry| instance_eval("\"#{entry_format}\"")}.join(', ')
+ message = options.delete(:message) || 'is invalid, must be one of #{entries}'
+ options[:message] = instance_eval("\"#{message}\"")
+ validates_format_of(context[:attr], options)
+ end
+ end
+ end
+
+
+ end
+ end
+end
+
+
--- /dev/null
+require 'selectable_attr_rails/validatable'
+
+module SelectableAttrRails
+ module Validatable
+ module Enum
+ def validates_format_options
+ @validates_format_options
+ end
+ def validates_format(options = nil)
+ return @validates_format_options = nil if options == false
+ @validates_format_options = options || {}
+ end
+
+ end
+ end
+end
--- /dev/null
+module SelectableAttrRails
+ VERSION = '0.0.3'
+end
--- /dev/null
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{selectable_attr_rails}
+ s.version = "0.3.7"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Takeshi Akima"]
+ s.date = %q{2009-08-17}
+ s.description = %q{selectable_attr_rails makes possible to use selectable_attr in rails application}
+ s.email = %q{akima@gmail.com}
+ s.extra_rdoc_files = [
+ "README"
+ ]
+ s.files = [
+ ".gitignore",
+ "MIT-LICENSE",
+ "README",
+ "Rakefile",
+ "VERSION.yml",
+ "init.rb",
+ "install.rb",
+ "lib/selectable_attr_i18n.rb",
+ "lib/selectable_attr_rails.rb",
+ "lib/selectable_attr_rails/db_loadable.rb",
+ "lib/selectable_attr_rails/helpers.rb",
+ "lib/selectable_attr_rails/helpers/abstract_selection_helper.rb",
+ "lib/selectable_attr_rails/helpers/check_box_group_helper.rb",
+ "lib/selectable_attr_rails/helpers/radio_button_group_helper.rb",
+ "lib/selectable_attr_rails/helpers/select_helper.rb",
+ "lib/selectable_attr_rails/version.rb",
+ "selectable_attr_rails.gemspec",
+ "spec/database.yml",
+ "spec/fixtures/.gitignore",
+ "spec/introduction_spec.rb",
+ "spec/schema.rb",
+ "spec/selectable_attr_i18n_spec.rb",
+ "spec/spec_helper.rb",
+ "uninstall.rb"
+ ]
+ s.homepage = %q{http://github.com/akm/selectable_attr_rails/}
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.4}
+ s.summary = %q{selectable_attr_rails makes possible to use selectable_attr in rails application}
+ s.test_files = [
+ "spec/introduction_spec.rb",
+ "spec/schema.rb",
+ "spec/selectable_attr_i18n_spec.rb",
+ "spec/spec_helper.rb"
+ ]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.0.2"])
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.0.2"])
+ s.add_runtime_dependency(%q<actionpack>, [">= 2.0.2"])
+ s.add_runtime_dependency(%q<akm-selectable_attr>, [">= 0.3.5"])
+ else
+ s.add_dependency(%q<activesupport>, [">= 2.0.2"])
+ s.add_dependency(%q<activerecord>, [">= 2.0.2"])
+ s.add_dependency(%q<actionpack>, [">= 2.0.2"])
+ s.add_dependency(%q<akm-selectable_attr>, [">= 0.3.5"])
+ end
+ else
+ s.add_dependency(%q<activesupport>, [">= 2.0.2"])
+ s.add_dependency(%q<activerecord>, [">= 2.0.2"])
+ s.add_dependency(%q<actionpack>, [">= 2.0.2"])
+ s.add_dependency(%q<akm-selectable_attr>, [">= 0.3.5"])
+ end
+end
--- /dev/null
+sqlite:
+ :adapter: sqlite
+ :database: selectable_attr_test.sqlite.db
+sqlite3:
+ :adapter: sqlite3
+ :database: selectable_attr_test.sqlite3.db
--- /dev/null
+# -*- coding: utf-8 -*-
+require File.join(File.dirname(__FILE__), 'spec_helper')
+
+describe SelectableAttr do
+
+ def assert_product_discount(klass)
+ # productsテーブルのデータから安売り用の価格は
+ # product_type_cd毎に決められた割合をpriceにかけて求めます。
+ p1 = klass.new(:name => '実践Rails', :product_type_cd => '01', :price => 3000)
+ p1.discount_price.should == 2400
+ p2 = klass.new(:name => '薔薇の名前', :product_type_cd => '02', :price => 1500)
+ p2.discount_price.should == 300
+ p3 = klass.new(:name => '未来派野郎', :product_type_cd => '03', :price => 3000)
+ p3.discount_price.should == 1500
+ end
+
+ # 定数をガンガン定義した場合
+ # 大文字が多くて読みにくいし、関連するデータ(ここではDISCOUNT)が増える毎に定数も増えていきます。
+ class LegacyProduct1 < ActiveRecord::Base
+ set_table_name 'products'
+
+ PRODUCT_TYPE_BOOK = '01'
+ PRODUCT_TYPE_DVD = '02'
+ PRODUCT_TYPE_CD = '03'
+ PRODUCT_TYPE_OTHER = '09'
+
+ PRODUCT_TYPE_OPTIONS = [
+ ['書籍', PRODUCT_TYPE_BOOK],
+ ['DVD', PRODUCT_TYPE_DVD],
+ ['CD', PRODUCT_TYPE_CD],
+ ['その他', PRODUCT_TYPE_OTHER]
+ ]
+
+ DISCOUNT = {
+ PRODUCT_TYPE_BOOK => 0.8,
+ PRODUCT_TYPE_DVD => 0.2,
+ PRODUCT_TYPE_CD => 0.5,
+ PRODUCT_TYPE_OTHER => 1
+ }
+
+ def discount_price
+ (DISCOUNT[product_type_cd] * price).to_i
+ end
+ end
+
+ it "test_legacy_product" do
+ assert_product_discount(LegacyProduct1)
+
+ # 選択肢を表示するためのデータは以下のように取得できる
+ LegacyProduct1::PRODUCT_TYPE_OPTIONS.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+ end
+
+
+
+
+ # できるだけ定数定義をまとめた場合
+ # 結構すっきりするけど、同じことをいろんなモデルで書くかと思うと気が重い。
+ class LegacyProduct2 < ActiveRecord::Base
+ set_table_name 'products'
+
+ PRODUCT_TYPE_DEFS = [
+ {:id => '01', :name => '書籍', :discount => 0.8},
+ {:id => '02', :name => 'DVD', :discount => 0.2},
+ {:id => '03', :name => 'CD', :discount => 0.5},
+ {:id => '09', :name => 'その他', :discount => 1}
+ ]
+
+ PRODUCT_TYPE_OPTIONS = PRODUCT_TYPE_DEFS.map{|t| [t[:name], t[:id]]}
+ DISCOUNT = PRODUCT_TYPE_DEFS.inject({}){|dest, t|
+ dest[t[:id]] = t[:discount]; dest}
+
+ def discount_price
+ (DISCOUNT[product_type_cd] * price).to_i
+ end
+ end
+
+ it "test_legacy_product" do
+ assert_product_discount(LegacyProduct2)
+
+ # 選択肢を表示するためのデータは以下のように取得できる
+ LegacyProduct2::PRODUCT_TYPE_OPTIONS.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+ end
+
+ # selectable_attrを使った場合
+ # 定義は一カ所にまとめられて、任意の属性(ここでは:discount)も一緒に書くことができてすっきり〜
+ class Product1 < ActiveRecord::Base
+ set_table_name 'products'
+
+ selectable_attr :product_type_cd do
+ entry '01', :book, '書籍', :discount => 0.8
+ entry '02', :dvd, 'DVD', :discount => 0.2
+ entry '03', :cd, 'CD', :discount => 0.5
+ entry '09', :other, 'その他', :discount => 1
+ validates_format :allow_nil => true, :message => 'は次のいずれかでなければなりません。 #{entries}'
+ end
+
+ def discount_price
+ (product_type_entry[:discount] * price).to_i
+ end
+ end
+
+ it "test_product1" do
+ assert_product_discount(Product1)
+ # 選択肢を表示するためのデータは以下のように取得できる
+ Product1.product_type_options.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+ end
+
+
+ # selectable_attrが定義するインスタンスメソッドの詳細
+ it "test_product_type_instance_methods" do
+ p1 = Product1.new
+ p1.product_type_cd.should be_nil
+ p1.product_type_key.should be_nil
+ p1.product_type_name.should be_nil
+ # idを変更すると得られるキーも名称も変わります
+ p1.product_type_cd = '02'
+ p1.product_type_cd.should == '02'
+ p1.product_type_key.should == :dvd
+ p1.product_type_name.should == 'DVD'
+ # キーを変更すると得られるidも名称も変わります
+ p1.product_type_key = :book
+ p1.product_type_cd.should == '01'
+ p1.product_type_key.should == :book
+ p1.product_type_name.should == '書籍'
+ # id、キー、名称以外の任意の属性は、entryの[]メソッドで取得します。
+ p1.product_type_key = :cd
+ p1.product_type_entry[:discount].should == 0.5
+ end
+
+ # selectable_attrが定義するクラスメソッドの詳細
+ it "test_product_type_class_methods" do
+ # キーからid、名称を取得できます
+ Product1.product_type_id_by_key(:book).should == '01'
+ Product1.product_type_id_by_key(:dvd).should == '02'
+ Product1.product_type_id_by_key(:cd).should == '03'
+ Product1.product_type_id_by_key(:other).should == '09'
+ Product1.product_type_name_by_key(:book).should == '書籍'
+ Product1.product_type_name_by_key(:dvd).should == 'DVD'
+ Product1.product_type_name_by_key(:cd).should == 'CD'
+ Product1.product_type_name_by_key(:other).should == 'その他'
+ # 存在しないキーの場合はnilを返します
+ Product1.product_type_id_by_key(nil).should be_nil
+ Product1.product_type_name_by_key(nil).should be_nil
+ Product1.product_type_id_by_key(:unexist).should be_nil
+ Product1.product_type_name_by_key(:unexist).should be_nil
+
+ # idからキー、名称を取得できます
+ Product1.product_type_key_by_id('01').should == :book
+ Product1.product_type_key_by_id('02').should == :dvd
+ Product1.product_type_key_by_id('03').should == :cd
+ Product1.product_type_key_by_id('09').should == :other
+ Product1.product_type_name_by_id('01').should == '書籍'
+ Product1.product_type_name_by_id('02').should == 'DVD'
+ Product1.product_type_name_by_id('03').should == 'CD'
+ Product1.product_type_name_by_id('09').should == 'その他'
+ # 存在しないidの場合はnilを返します
+ Product1.product_type_key_by_id(nil).should be_nil
+ Product1.product_type_name_by_id(nil).should be_nil
+ Product1.product_type_key_by_id('99').should be_nil
+ Product1.product_type_name_by_id('99').should be_nil
+
+ # id、キー、名称の配列を取得できます
+ Product1.product_type_ids.should == ['01', '02', '03', '09']
+ Product1.product_type_keys.should == [:book, :dvd, :cd, :other]
+ Product1.product_type_names.should == ['書籍', 'DVD', 'CD', 'その他']
+ # 一部のものだけ取得することも可能です。
+ Product1.product_type_ids(:cd, :dvd).should == ['03', '02' ]
+ Product1.product_type_keys('02', '03').should == [:dvd, :cd ]
+ Product1.product_type_names('02', '03').should == ['DVD', 'CD']
+ Product1.product_type_names(:cd, :dvd).should == ['CD', 'DVD']
+
+ # select_tagなどのoption_tagsを作るための配列なんか一発っす
+ Product1.product_type_options.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+ end
+
+ it "validate with entries" do
+ p1 = Product1.new
+ p1.product_type_cd.should == nil
+ p1.valid?.should == true
+ p1.errors.empty?.should == true
+
+ p1.product_type_key = :book
+ p1.product_type_cd.should == '01'
+ p1.valid?.should == true
+ p1.errors.empty?.should == true
+
+ p1.product_type_cd = 'XX'
+ p1.product_type_cd.should == 'XX'
+ p1.valid?.should == false
+ p1.errors.on(:product_type_cd).should == "は次のいずれかでなければなりません。 書籍, DVD, CD, その他"
+ end
+
+ # selectable_attrのエントリ名をDB上に保持するためのモデル
+ class ItemMaster < ActiveRecord::Base
+ end
+
+ # selectable_attrを使った場合その2
+ # アクセス時に毎回アクセス時にDBから項目名を取得します。
+ class ProductWithDB1 < ActiveRecord::Base
+ set_table_name 'products'
+
+ selectable_attr :product_type_cd do
+ update_by(
+ "select item_cd, name from item_masters where category_name = 'product_type_cd' order by item_no",
+ :when => :everytime)
+ entry '01', :book, '書籍', :discount => 0.8
+ entry '02', :dvd, 'DVD', :discount => 0.2
+ entry '03', :cd, 'CD', :discount => 0.5
+ entry '09', :other, 'その他', :discount => 1
+ end
+
+ def discount_price
+ (product_type_entry[:discount] * price).to_i
+ end
+ end
+
+ it "test_update_entry_name" do
+ # DBに全くデータがなくてもコードで記述してあるエントリは存在します。
+ ItemMaster.delete_all("category_name = 'product_type_cd'")
+ ProductWithDB1.product_type_entries.length.should == 4
+ ProductWithDB1.product_type_name_by_key(:book).should == '書籍'
+ ProductWithDB1.product_type_name_by_key(:dvd).should == 'DVD'
+ ProductWithDB1.product_type_name_by_key(:cd).should == 'CD'
+ ProductWithDB1.product_type_name_by_key(:other).should == 'その他'
+
+ assert_product_discount(ProductWithDB1)
+
+ # DBからエントリの名称を動的に変更できます
+ item_book = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 1, :item_cd => '01', :name => '本')
+ ProductWithDB1.product_type_entries.length.should == 4
+ ProductWithDB1.product_type_name_by_key(:book).should == '本'
+ ProductWithDB1.product_type_name_by_key(:dvd).should == 'DVD'
+ ProductWithDB1.product_type_name_by_key(:cd).should == 'CD'
+ ProductWithDB1.product_type_name_by_key(:other).should == 'その他'
+ ProductWithDB1.product_type_options.should ==
+ [['本', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+
+ # DBからエントリの並び順を動的に変更できます
+ item_book.item_no = 4;
+ item_book.save!
+ item_other = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 1, :item_cd => '09', :name => 'その他')
+ item_dvd = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 2, :item_cd => '02') # nameは指定しなかったらデフォルトが使われます。
+ item_cd = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 3, :item_cd => '03') # nameは指定しなかったらデフォルトが使われます。
+ ProductWithDB1.product_type_options.should ==
+ [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01']]
+
+ # DBからエントリを動的に追加することも可能です。
+ item_toys = ItemMaster.create(:category_name => 'product_type_cd', :item_no => 5, :item_cd => '04', :name => 'おもちゃ')
+ ProductWithDB1.product_type_options.should ==
+ [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04']]
+ ProductWithDB1.product_type_key_by_id('04').should == :entry_04
+
+ # DBからレコードを削除してもコードで定義したentryは削除されません。
+ # 順番はDBからの取得順で並び替えられたものの後になります
+ item_dvd.destroy
+ ProductWithDB1.product_type_options.should ==
+ [['その他', '09'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04'], ['DVD', '02']]
+
+ # DB上で追加したレコードを削除すると、エントリも削除されます
+ item_toys.destroy
+ ProductWithDB1.product_type_options.should ==
+ [['その他', '09'], ['CD', '03'], ['本', '01'], ['DVD', '02']]
+
+ # 名称を指定していたDBのレコードを削除したら元に戻ります。
+ item_book.destroy
+ ProductWithDB1.product_type_options.should ==
+ [['その他', '09'], ['CD', '03'], ['書籍', '01'], ['DVD', '02']]
+
+ # エントリに該当するレコードを全部削除したら、元に戻ります。
+ ItemMaster.delete_all("category_name = 'product_type_cd'")
+ ProductWithDB1.product_type_options.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+
+ assert_product_discount(ProductWithDB1)
+ end
+
+
+
+
+ # Q: product_type_cd の'_cd'はどこにいっちゃったの?
+ # A: デフォルトでは、/(_cd$|_code$|_cds$|_codes$)/ を削除したものをbase_nameとして
+ # 扱い、それに_keyなどを付加してメソッド名を定義します。もしこのルールを変更したい場合、
+ # selectable_attrを使う前に selectable_attr_name_pattern で新たなルールを指定してください。
+ class Product2 < ActiveRecord::Base
+ set_table_name 'products'
+ self.selectable_attr_name_pattern = /^product_|_cd$/
+
+ selectable_attr :product_type_cd do
+ entry '01', :book, '書籍', :discount => 0.8
+ entry '02', :dvd, 'DVD', :discount => 0.2
+ entry '03', :cd, 'CD', :discount => 0.5
+ entry '09', :other, 'その他', :discount => 1
+ end
+
+ def discount_price
+ (type_entry[:discount] * price).to_i
+ end
+ end
+
+ it "test_product2" do
+ assert_product_discount(Product2)
+ # 選択肢を表示するためのデータは以下のように取得できる
+ Product2.type_options.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+
+ p2 = Product2.new
+ p2.product_type_cd.should be_nil
+ p2.type_key.should be_nil
+ p2.type_name.should be_nil
+ # idを変更すると得られるキーも名称も変わります
+ p2.product_type_cd = '02'
+ p2.product_type_cd.should == '02'
+ p2.type_key.should == :dvd
+ p2.type_name.should == 'DVD'
+ # キーを変更すると得られるidも名称も変わります
+ p2.type_key = :book
+ p2.product_type_cd.should == '01'
+ p2.type_key.should == :book
+ p2.type_name.should == '書籍'
+ # id、キー、名称以外の任意の属性は、entryの[]メソッドで取得します。
+ p2.type_key = :cd
+ p2.type_entry[:discount].should == 0.5
+
+ Product2.type_id_by_key(:book).should == '01'
+ Product2.type_id_by_key(:dvd).should == '02'
+ Product2.type_name_by_key(:cd).should == 'CD'
+ Product2.type_name_by_key(:other).should == 'その他'
+ Product2.type_key_by_id('09').should == :other
+ Product2.type_name_by_id('01').should == '書籍'
+ Product2.type_keys.should == [:book, :dvd, :cd, :other]
+ Product2.type_names.should == ['書籍', 'DVD', 'CD', 'その他']
+ Product2.type_keys('02', '03').should == [:dvd, :cd]
+ Product2.type_names(:cd, :dvd).should == ['CD', 'DVD']
+ end
+
+
+
+
+ # Q: selectable_attrの呼び出し毎にbase_bname(って言うの?)を指定したいんだけど。
+ # A: base_nameオプションを指定してください。
+ class Product3 < ActiveRecord::Base
+ set_table_name 'products'
+
+ selectable_attr :product_type_cd, :base_name => 'type' do
+ entry '01', :book, '書籍', :discount => 0.8
+ entry '02', :dvd, 'DVD', :discount => 0.2
+ entry '03', :cd, 'CD', :discount => 0.5
+ entry '09', :other, 'その他', :discount => 1
+ end
+
+ def discount_price
+ (type_entry[:discount] * price).to_i
+ end
+ end
+
+ it "test_product3" do
+ assert_product_discount(Product3)
+ # 選択肢を表示するためのデータは以下のように取得できる
+ Product3.type_options.should ==
+ [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+
+ p3 = Product3.new
+ p3.product_type_cd.should be_nil
+ p3.type_key.should be_nil
+ p3.type_name.should be_nil
+ # idを変更すると得られるキーも名称も変わります
+ p3.product_type_cd = '02'
+ p3.product_type_cd.should == '02'
+ p3.type_key.should == :dvd
+ p3.type_name.should == 'DVD'
+ # キーを変更すると得られるidも名称も変わります
+ p3.type_key = :book
+ p3.product_type_cd.should == '01'
+ p3.type_key.should == :book
+ p3.type_name.should == '書籍'
+ # id、キー、名称以外の任意の属性は、entryの[]メソッドで取得します。
+ p3.type_key = :cd
+ p3.type_entry[:discount].should == 0.5
+
+ Product3.type_id_by_key(:book).should == '01'
+ Product3.type_id_by_key(:dvd).should == '02'
+ Product3.type_name_by_key(:cd).should == 'CD'
+ Product3.type_name_by_key(:other).should == 'その他'
+ Product3.type_key_by_id('09').should == :other
+ Product3.type_name_by_id('01').should == '書籍'
+ Product3.type_keys.should == [:book, :dvd, :cd, :other]
+ Product3.type_names.should == ['書籍', 'DVD', 'CD', 'その他']
+ Product3.type_keys('02', '03').should == [:dvd, :cd]
+ Product3.type_names(:cd, :dvd).should == ['CD', 'DVD']
+ end
+
+end
+
--- /dev/null
+ActiveRecord::Schema.define(:version => 1) do
+
+ create_table "products", :force => true do |t|
+ t.string "product_type_cd"
+ t.integer "price"
+ t.string "name"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "item_masters", :force => true do |t|
+ t.string "category_name", :limit => 20
+ t.integer "item_no"
+ t.string "item_cd", :limit => 2
+ t.string "name", :limit => 20
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "i18n_item_masters", :force => true do |t|
+ t.string "locale", :limit => 5
+ t.string "category_name", :limit => 20
+ t.integer "item_no"
+ t.string "item_cd", :limit => 2
+ t.string "name", :limit => 20
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+end
--- /dev/null
+# -*- coding: utf-8 -*-
+if defined?(I18n)
+ require File.join(File.dirname(__FILE__), 'spec_helper')
+
+ describe SelectableAttr::Enum do
+
+ before(:each) do
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations 'en', 'selectable_attrs' => {'enum1' => {
+ 'entry1' => 'entry one',
+ 'entry2' => 'entry two',
+ 'entry3' => 'entry three'
+ } }
+ I18n.backend.store_translations 'ja', 'selectable_attrs' => {'enum1' => {
+ 'entry1' => 'エントリ壱',
+ 'entry2' => 'エントリ弐',
+ 'entry3' => 'エントリ参'
+ } }
+ end
+
+ Enum1 = SelectableAttr::Enum.new do
+ i18n_scope(:selectable_attrs, :enum1)
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+
+ it 'test_enum1_i18n' do
+ I18n.locale = nil
+ I18n.locale.should == :en
+ Enum1.name_by_key(:entry1).should == "entry one"
+ Enum1.name_by_key(:entry2).should == "entry two"
+ Enum1.name_by_key(:entry3).should == "entry three"
+ Enum1.names.should == ["entry one", "entry two", "entry three"]
+
+ I18n.locale = 'ja'
+ Enum1.name_by_key(:entry1).should == "エントリ壱"
+ Enum1.name_by_key(:entry2).should == "エントリ弐"
+ Enum1.name_by_key(:entry3).should == "エントリ参"
+ Enum1.names.should == ["エントリ壱", "エントリ弐", "エントリ参"]
+
+ I18n.locale = 'en'
+ Enum1.name_by_key(:entry1).should == "entry one"
+ Enum1.name_by_key(:entry2).should == "entry two"
+ Enum1.name_by_key(:entry3).should == "entry three"
+ Enum1.names.should == ["entry one", "entry two", "entry three"]
+ end
+
+ class EnumBase
+ include ::SelectableAttr::Base
+ end
+
+ class SelectableAttrMock1 < EnumBase
+ selectable_attr :attr1, :default => 2 do
+ i18n_scope(:selectable_attrs, :enum1)
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it 'test_attr1_i18n' do
+ I18n.default_locale = 'ja'
+ I18n.locale = nil
+ I18n.locale.should == 'ja'
+ SelectableAttrMock1.attr1_name_by_key(:entry1).should == "エントリ壱"
+ SelectableAttrMock1.attr1_name_by_key(:entry2).should == "エントリ弐"
+ SelectableAttrMock1.attr1_name_by_key(:entry3).should == "エントリ参"
+ SelectableAttrMock1.attr1_options.should == [["エントリ壱",1], ["エントリ弐",2], ["エントリ参",3]]
+
+ I18n.locale = 'ja'
+ SelectableAttrMock1.attr1_name_by_key(:entry1).should == "エントリ壱"
+ SelectableAttrMock1.attr1_name_by_key(:entry2).should == "エントリ弐"
+ SelectableAttrMock1.attr1_name_by_key(:entry3).should == "エントリ参"
+ SelectableAttrMock1.attr1_options.should == [["エントリ壱",1], ["エントリ弐",2], ["エントリ参",3]]
+
+ I18n.locale = 'en'
+ SelectableAttrMock1.attr1_name_by_key(:entry1).should == "entry one"
+ SelectableAttrMock1.attr1_name_by_key(:entry2).should == "entry two"
+ SelectableAttrMock1.attr1_name_by_key(:entry3).should == "entry three"
+ SelectableAttrMock1.attr1_options.should == [["entry one",1], ["entry two",2], ["entry three",3]]
+ end
+
+ class SelectableAttrMock2 < EnumBase
+ selectable_attr :enum1, :default => 2 do
+ i18n_scope(:selectable_attrs, :enum1)
+ entry 1, :entry1, "エントリ1"
+ entry 2, :entry2, "エントリ2"
+ entry 3, :entry3, "エントリ3"
+ end
+ end
+
+ it 'test_enum1_i18n' do
+ I18n.default_locale = 'ja'
+ I18n.locale = nil
+ I18n.locale.should == 'ja'
+ SelectableAttrMock2.enum1_name_by_key(:entry1).should == "エントリ壱"
+ SelectableAttrMock2.enum1_name_by_key(:entry2).should == "エントリ弐"
+ SelectableAttrMock2.enum1_name_by_key(:entry3).should == "エントリ参"
+ SelectableAttrMock2.enum1_options.should == [["エントリ壱",1], ["エントリ弐",2], ["エントリ参",3]]
+
+ I18n.locale = 'ja'
+ SelectableAttrMock2.enum1_name_by_key(:entry1).should == "エントリ壱"
+ SelectableAttrMock2.enum1_name_by_key(:entry2).should == "エントリ弐"
+ SelectableAttrMock2.enum1_name_by_key(:entry3).should == "エントリ参"
+ SelectableAttrMock2.enum1_options.should == [["エントリ壱",1], ["エントリ弐",2], ["エントリ参",3]]
+
+ I18n.locale = 'en'
+ SelectableAttrMock2.enum1_name_by_key(:entry1).should == "entry one"
+ SelectableAttrMock2.enum1_name_by_key(:entry2).should == "entry two"
+ SelectableAttrMock2.enum1_name_by_key(:entry3).should == "entry three"
+ SelectableAttrMock2.enum1_options.should == [["entry one",1], ["entry two",2], ["entry three",3]]
+ end
+
+ # i18n用のlocaleカラムを持つselectable_attrのエントリ名をDB上に保持するためのモデル
+ class I18nItemMaster < ActiveRecord::Base
+ end
+
+ # selectable_attrを使った場合その3
+ # アクセス時に毎回アクセス時にDBから項目名を取得します。
+ # 対象となる項目名はi18n対応している名称です
+ class ProductWithI18nDB1 < ActiveRecord::Base
+ set_table_name 'products'
+ selectable_attr :product_type_cd do
+ # update_byメソッドには、エントリのidと名称を返すSELECT文を指定する代わりに、
+ # エントリのidと名称の配列の配列を返すブロックを指定することも可能です。
+ update_by(:when => :everytime) do
+ records = I18nItemMaster.find(:all,
+ :conditions => [
+ "category_name = 'product_type_cd' and locale = ? ", I18n.locale.to_s],
+ :order => "item_no")
+ records.map{|r| [r.item_cd, r.name]}
+ end
+ entry '01', :book, '書籍', :discount => 0.8
+ entry '02', :dvd, 'DVD', :discount => 0.2
+ entry '03', :cd, 'CD', :discount => 0.5
+ entry '09', :other, 'その他', :discount => 1
+ end
+
+ end
+
+ it "test_update_entry_name_with_i18n" do
+ I18n.locale = 'ja'
+ # DBに全くデータがなくてもコードで記述してあるエントリは存在します。
+ I18nItemMaster.delete_all("category_name = 'product_type_cd'")
+ ProductWithI18nDB1.product_type_entries.length.should == 4
+ ProductWithI18nDB1.product_type_name_by_key(:book).should == '書籍'
+ ProductWithI18nDB1.product_type_name_by_key(:dvd).should == 'DVD'
+ ProductWithI18nDB1.product_type_name_by_key(:cd).should == 'CD'
+ ProductWithI18nDB1.product_type_name_by_key(:other).should == 'その他'
+
+ ProductWithI18nDB1.product_type_hash_array.should == [
+ {:id => '01', :key => :book, :name => '書籍', :discount => 0.8},
+ {:id => '02', :key => :dvd, :name => 'DVD', :discount => 0.2},
+ {:id => '03', :key => :cd, :name => 'CD', :discount => 0.5},
+ {:id => '09', :key => :other, :name => 'その他', :discount => 1},
+ ]
+
+ # DBからエントリの名称を動的に変更できます
+ item_book = I18nItemMaster.create(:locale => 'ja', :category_name => 'product_type_cd', :item_no => 1, :item_cd => '01', :name => '本')
+ ProductWithI18nDB1.product_type_entries.length.should == 4
+ ProductWithI18nDB1.product_type_name_by_key(:book).should == '本'
+ ProductWithI18nDB1.product_type_name_by_key(:dvd).should == 'DVD'
+ ProductWithI18nDB1.product_type_name_by_key(:cd).should == 'CD'
+ ProductWithI18nDB1.product_type_name_by_key(:other).should == 'その他'
+ ProductWithI18nDB1.product_type_options.should == [['本', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+
+ ProductWithI18nDB1.product_type_hash_array.should == [
+ {:id => '01', :key => :book, :name => '本', :discount => 0.8},
+ {:id => '02', :key => :dvd, :name => 'DVD', :discount => 0.2},
+ {:id => '03', :key => :cd, :name => 'CD', :discount => 0.5},
+ {:id => '09', :key => :other, :name => 'その他', :discount => 1},
+ ]
+
+ # DBからエントリの並び順を動的に変更できます
+ item_book.item_no = 4;
+ item_book.save!
+ item_other = I18nItemMaster.create(:locale => 'ja', :category_name => 'product_type_cd', :item_no => 1, :item_cd => '09', :name => 'その他')
+ item_dvd = I18nItemMaster.create(:locale => 'ja', :category_name => 'product_type_cd', :item_no => 2, :item_cd => '02') # nameは指定しなかったらデフォルトが使われます。
+ item_cd = I18nItemMaster.create(:locale => 'ja', :category_name => 'product_type_cd', :item_no => 3, :item_cd => '03') # nameは指定しなかったらデフォルトが使われます。
+ ProductWithI18nDB1.product_type_options.should == [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01']]
+
+ # DBからエントリを動的に追加することも可能です。
+ item_toys = I18nItemMaster.create(:locale => 'ja', :category_name => 'product_type_cd', :item_no => 5, :item_cd => '04', :name => 'おもちゃ')
+ ProductWithI18nDB1.product_type_options.should == [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04']]
+ ProductWithI18nDB1.product_type_key_by_id('04').should == :entry_04
+
+ ProductWithI18nDB1.product_type_hash_array.should == [
+ {:id => '09', :key => :other, :name => 'その他', :discount => 1},
+ {:id => '02', :key => :dvd, :name => 'DVD', :discount => 0.2},
+ {:id => '03', :key => :cd, :name => 'CD', :discount => 0.5},
+ {:id => '01', :key => :book, :name => '本', :discount => 0.8},
+ {:id => '04', :key => :entry_04, :name => 'おもちゃ'}
+ ]
+
+
+ # 英語名を登録
+ item_book = I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 4, :item_cd => '01', :name => 'Book')
+ item_other = I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 1, :item_cd => '09', :name => 'Others')
+ item_dvd = I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 2, :item_cd => '02', :name => 'DVD')
+ item_cd = I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 3, :item_cd => '03', :name => 'CD')
+ item_toys = I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 5, :item_cd => '04', :name => 'Toy')
+
+ # 英語名が登録されていてもI18n.localeが変わらなければ、日本語のまま
+ ProductWithI18nDB1.product_type_options.should == [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04']]
+ ProductWithI18nDB1.product_type_key_by_id('04').should == :entry_04
+
+ ProductWithI18nDB1.product_type_hash_array.should == [
+ {:id => '09', :key => :other, :name => 'その他', :discount => 1},
+ {:id => '02', :key => :dvd, :name => 'DVD', :discount => 0.2},
+ {:id => '03', :key => :cd, :name => 'CD', :discount => 0.5},
+ {:id => '01', :key => :book, :name => '本', :discount => 0.8},
+ {:id => '04', :key => :entry_04, :name => 'おもちゃ'}
+ ]
+
+ # I18n.localeを変更すると取得できるエントリの名称も変わります
+ I18n.locale = 'en'
+ ProductWithI18nDB1.product_type_options.should == [['Others', '09'], ['DVD', '02'], ['CD', '03'], ['Book', '01'], ['Toy', '04']]
+ ProductWithI18nDB1.product_type_key_by_id('04').should == :entry_04
+
+ ProductWithI18nDB1.product_type_hash_array.should == [
+ {:id => '09', :key => :other, :name => 'Others', :discount => 1},
+ {:id => '02', :key => :dvd, :name => 'DVD', :discount => 0.2},
+ {:id => '03', :key => :cd, :name => 'CD', :discount => 0.5},
+ {:id => '01', :key => :book, :name => 'Book', :discount => 0.8},
+ {:id => '04', :key => :entry_04, :name => 'Toy'}
+ ]
+
+ I18n.locale = 'ja'
+ ProductWithI18nDB1.product_type_options.should == [['その他', '09'], ['DVD', '02'], ['CD', '03'], ['本', '01'], ['おもちゃ', '04']]
+ ProductWithI18nDB1.product_type_key_by_id('04').should == :entry_04
+
+ I18n.locale = 'en'
+ ProductWithI18nDB1.product_type_options.should == [['Others', '09'], ['DVD', '02'], ['CD', '03'], ['Book', '01'], ['Toy', '04']]
+ ProductWithI18nDB1.product_type_key_by_id('04').should == :entry_04
+
+ # DBからレコードを削除してもコードで定義したentryは削除されません。
+ # 順番はDBからの取得順で並び替えられたものの後になります
+ item_dvd.destroy
+ ProductWithI18nDB1.product_type_options.should == [['Others', '09'], ['CD', '03'], ['Book', '01'], ['Toy', '04'], ['DVD', '02']]
+
+ # DB上で追加したレコードを削除すると、エントリも削除されます
+ item_toys.destroy
+ ProductWithI18nDB1.product_type_options.should == [['Others', '09'], ['CD', '03'], ['Book', '01'], ['DVD', '02']]
+
+ # 名称を指定していたDBのレコードを削除したら元に戻ります。
+ item_book.destroy
+ ProductWithI18nDB1.product_type_options.should == [['Others', '09'], ['CD', '03'], ['書籍', '01'], ['DVD', '02']]
+
+ # エントリに該当するレコードを全部削除したら、元に戻ります。
+ I18nItemMaster.delete_all("category_name = 'product_type_cd'")
+ ProductWithI18nDB1.product_type_options.should == [['書籍', '01'], ['DVD', '02'], ['CD', '03'], ['その他', '09']]
+ end
+
+ it 'test_i18n_export' do
+ io = StringIO.new
+ SelectableAttrRails.logger = Logger.new(io)
+
+ I18nItemMaster.delete_all("category_name = 'product_type_cd'")
+
+ I18n.locale = 'ja'
+ actual = SelectableAttr::Enum.i18n_export
+ actual.keys.should == ['selectable_attrs']
+ actual['selectable_attrs'].keys.include?('enum1').should == true
+ actual['selectable_attrs']['enum1'].should ==
+ {'entry1'=>"エントリ壱",
+ 'entry2'=>"エントリ弐",
+ 'entry3'=>"エントリ参"}
+
+ actual['selectable_attrs']['ProductWithI18nDB1'].should ==
+ {'product_type_cd'=>
+ {'book'=>"書籍", 'dvd'=>"DVD", 'cd'=>"CD", 'other'=>"その他"}}
+
+ I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 1, :item_cd => '09', :name => 'Others')
+ I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 2, :item_cd => '02', :name => 'DVD')
+ I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 3, :item_cd => '03', :name => 'CD')
+ I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 4, :item_cd => '01', :name => 'Book')
+ I18nItemMaster.create(:locale => 'en', :category_name => 'product_type_cd', :item_no => 5, :item_cd => '04', :name => 'Toy')
+
+ I18n.locale = 'en'
+ actual = SelectableAttr::Enum.i18n_export
+ actual.keys.should == ['selectable_attrs']
+ actual['selectable_attrs'].keys.include?('enum1').should == true
+ actual['selectable_attrs']['enum1'].should ==
+ {'entry1'=>"entry one",
+ 'entry2'=>"entry two",
+ 'entry3'=>"entry three"}
+ actual['selectable_attrs'].keys.should include('ProductWithI18nDB1')
+ actual['selectable_attrs']['ProductWithI18nDB1'].should ==
+ {'product_type_cd'=>
+ {'book'=>"Book", 'dvd'=>"DVD", 'cd'=>"CD", 'other'=>"Others", 'entry_04'=>"Toy"}}
+ end
+
+ Enum2 = SelectableAttr::Enum.new do
+ entry 1, :entry1, "縁鳥1"
+ entry 2, :entry2, "縁鳥2"
+ entry 3, :entry3, "縁鳥3"
+ end
+
+ it "i18n_scope missing" do
+ io = StringIO.new
+ SelectableAttrRails.logger = Logger.new(io)
+ actual = SelectableAttr::Enum.i18n_export([Enum2])
+ actual.inspect.should_not =~ /縁鳥/
+ io.rewind
+ io.readline.should =~ /^no\ i18n_scope\ of\ /
+ end
+ end
+else
+ $stderr.puts "WARNING! i18n test skipeed because I18n not found"
+end
--- /dev/null
+$KCODE='u'
+
+FIXTURES_ROOT = File.join(File.dirname(__FILE__), 'fixtures') unless defined?(FIXTURES_ROOT)
+
+require 'rubygems'
+require 'active_support'
+require 'active_record'
+require 'active_record/fixtures'
+require 'active_record/test_case'
+require 'action_view'
+
+require 'selectable_attr'
+
+$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
+require File.join(File.dirname(__FILE__), '..', 'init')
+
+require 'yaml'
+config = YAML.load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
+ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log'))
+ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
+
+load(File.join(File.dirname(__FILE__), 'schema.rb'))
+
+
+def assert_hash(expected, actual)
+ keys = (expected.keys + actual.keys).uniq
+ keys.each do |key|
+ assert_equal expected[key], actual[key], "unmatch value for #{key.inspect}"
+ end
+end
--- /dev/null
+# Uninstall hook code here