runo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. data/LICENSE +19 -0
  2. data/README.rdoc +120 -0
  3. data/bin/runo +35 -0
  4. data/lib/_error.rb +14 -0
  5. data/lib/_field.rb +260 -0
  6. data/lib/_i18n.rb +141 -0
  7. data/lib/_parser.rb +243 -0
  8. data/lib/_path.rb +86 -0
  9. data/lib/_storage/_storage.rb +213 -0
  10. data/lib/_storage/file.rb +200 -0
  11. data/lib/_storage/sequel.rb +174 -0
  12. data/lib/_storage/temp.rb +73 -0
  13. data/lib/_widget/action_create.rb +23 -0
  14. data/lib/_widget/action_login.rb +22 -0
  15. data/lib/_widget/action_signup.rb +16 -0
  16. data/lib/_widget/action_update.rb +16 -0
  17. data/lib/_widget/crumb.rb +24 -0
  18. data/lib/_widget/done.rb +16 -0
  19. data/lib/_widget/login.rb +25 -0
  20. data/lib/_widget/me.rb +31 -0
  21. data/lib/_widget/message.rb +51 -0
  22. data/lib/_widget/navi.rb +88 -0
  23. data/lib/_widget/submit.rb +49 -0
  24. data/lib/_widget/view_ym.rb +77 -0
  25. data/lib/_workflow/_workflow.rb +89 -0
  26. data/lib/_workflow/attachment.rb +50 -0
  27. data/lib/_workflow/blog.rb +28 -0
  28. data/lib/_workflow/contact.rb +23 -0
  29. data/lib/_workflow/forum.rb +26 -0
  30. data/lib/_workflow/register.rb +39 -0
  31. data/lib/meta/_meta.rb +20 -0
  32. data/lib/meta/group.rb +19 -0
  33. data/lib/meta/id.rb +59 -0
  34. data/lib/meta/owner.rb +21 -0
  35. data/lib/meta/timestamp.rb +118 -0
  36. data/lib/runo.rb +396 -0
  37. data/lib/scalar/checkbox.rb +68 -0
  38. data/lib/scalar/file.rb +144 -0
  39. data/lib/scalar/img.rb +112 -0
  40. data/lib/scalar/password.rb +58 -0
  41. data/lib/scalar/radio.rb +47 -0
  42. data/lib/scalar/select.rb +47 -0
  43. data/lib/scalar/text.rb +38 -0
  44. data/lib/scalar/textarea.rb +35 -0
  45. data/lib/scalar/textarea_pre.rb +14 -0
  46. data/lib/scalar/textarea_wiki.rb +173 -0
  47. data/lib/set/_set.rb +195 -0
  48. data/lib/set/dynamic.rb +177 -0
  49. data/lib/set/static.rb +102 -0
  50. data/lib/set/static_folder.rb +96 -0
  51. data/locale/en/index.po +242 -0
  52. data/locale/index.pot +243 -0
  53. data/locale/ja/index.po +242 -0
  54. data/locale/lazy_parser.rb +54 -0
  55. data/skel/config.ru +27 -0
  56. data/skel/skin/_users/00000000_frank-avatar.jpg +0 -0
  57. data/skel/skin/_users/00000000_frank-avatar_small.jpg +0 -0
  58. data/skel/skin/_users/00000000_frank.yaml +12 -0
  59. data/skel/skin/_users/00000000_root-avatar.jpg +0 -0
  60. data/skel/skin/_users/00000000_root-avatar_small.jpg +0 -0
  61. data/skel/skin/_users/00000000_root.yaml +11 -0
  62. data/skel/skin/_users/css/users.css +21 -0
  63. data/skel/skin/_users/css/users.less +25 -0
  64. data/skel/skin/_users/done.html +42 -0
  65. data/skel/skin/_users/index.html +46 -0
  66. data/skel/skin/_users/index.yaml +3 -0
  67. data/skel/skin/_users/summary.html +40 -0
  68. data/skel/skin/css/base.css +93 -0
  69. data/skel/skin/css/base.less +139 -0
  70. data/skel/skin/css/coax.css +199 -0
  71. data/skel/skin/css/coax.less +244 -0
  72. data/skel/skin/examples/blog/20091214_0001.yaml +8 -0
  73. data/skel/skin/examples/blog/20100630_0001.yaml +8 -0
  74. data/skel/skin/examples/blog/20100630_0002.yaml +14 -0
  75. data/skel/skin/examples/blog/20100701_0001.yaml +8 -0
  76. data/skel/skin/examples/blog/20100701_0002-a-20100701_0001-f.jpg +0 -0
  77. data/skel/skin/examples/blog/20100701_0002-a-20100701_0001-f_small.jpg +0 -0
  78. data/skel/skin/examples/blog/20100701_0002.yaml +19 -0
  79. data/skel/skin/examples/blog/frank/20100701_0001.yaml +10 -0
  80. data/skel/skin/examples/blog/frank/index.yaml +4 -0
  81. data/skel/skin/examples/blog/index.html +51 -0
  82. data/skel/skin/examples/blog/rss.xml +18 -0
  83. data/skel/skin/examples/contact/20100701_0001-file.txt +1 -0
  84. data/skel/skin/examples/contact/20100701_0001.yaml +15 -0
  85. data/skel/skin/examples/contact/20100701_0002.yaml +8 -0
  86. data/skel/skin/examples/contact/20100701_0003.yaml +9 -0
  87. data/skel/skin/examples/contact/index.html +47 -0
  88. data/skel/skin/examples/contact/js/contact.js +13 -0
  89. data/skel/skin/examples/contact/summary.html +54 -0
  90. data/skel/skin/examples/forum/20100701_0001.yaml +41 -0
  91. data/skel/skin/examples/forum/20100701_0002.yaml +25 -0
  92. data/skel/skin/examples/forum/index.html +68 -0
  93. data/skel/skin/examples/forum/summary.html +47 -0
  94. data/skel/skin/examples/index.html +75 -0
  95. data/skel/skin/index.html +41 -0
  96. data/skel/skin/js/base.js +50 -0
  97. data/t/locale/de/index.po +19 -0
  98. data/t/locale/en-GB/index.po +25 -0
  99. data/t/locale/ja/index.po +30 -0
  100. data/t/skin/_users/00000000_test.yaml +3 -0
  101. data/t/skin/_users/index.html +13 -0
  102. data/t/skin/foo/20091120_0001.yaml +7 -0
  103. data/t/skin/foo/bar/20091120_0001.yaml +5 -0
  104. data/t/skin/foo/bar/index.yaml +5 -0
  105. data/t/skin/foo/baz/css/baz.css +1 -0
  106. data/t/skin/foo/css/foo.css +1 -0
  107. data/t/skin/foo/index.html +14 -0
  108. data/t/skin/foo/index.yaml +7 -0
  109. data/t/skin/foo/not_css/foo.css +1 -0
  110. data/t/skin/foo/sub-20100306_0001.yaml +3 -0
  111. data/t/skin/index.yaml +3 -0
  112. data/t/skin/t_attachment/index.html +13 -0
  113. data/t/skin/t_contact/done.html +6 -0
  114. data/t/skin/t_contact/index.html +9 -0
  115. data/t/skin/t_file/index.html +16 -0
  116. data/t/skin/t_img/index.html +14 -0
  117. data/t/skin/t_img/test.jpg +0 -0
  118. data/t/skin/t_select/index.html +9 -0
  119. data/t/skin/t_store/index.html +9 -0
  120. data/t/skin/t_summary/20100326_0001.yaml +3 -0
  121. data/t/skin/t_summary/create.html +9 -0
  122. data/t/skin/t_summary/index.html +9 -0
  123. data/t/skin/t_summary/summary.html +9 -0
  124. data/t/t.rb +27 -0
  125. data/t/test_checkbox.rb +273 -0
  126. data/t/test_field.rb +330 -0
  127. data/t/test_file.rb +900 -0
  128. data/t/test_id.rb +215 -0
  129. data/t/test_img.rb +328 -0
  130. data/t/test_meta.rb +57 -0
  131. data/t/test_parser.rb +1266 -0
  132. data/t/test_password.rb +188 -0
  133. data/t/test_radio.rb +226 -0
  134. data/t/test_role.rb +249 -0
  135. data/t/test_runo.rb +742 -0
  136. data/t/test_runo_call.rb +1286 -0
  137. data/t/test_runo_i18n.rb +318 -0
  138. data/t/test_select.rb +182 -0
  139. data/t/test_set_complex.rb +527 -0
  140. data/t/test_set_dynamic.rb +1504 -0
  141. data/t/test_set_folder.rb +515 -0
  142. data/t/test_set_permit.rb +246 -0
  143. data/t/test_set_static.rb +445 -0
  144. data/t/test_storage.rb +915 -0
  145. data/t/test_text.rb +125 -0
  146. data/t/test_textarea.rb +138 -0
  147. data/t/test_timestamp.rb +473 -0
  148. data/t/test_workflow.rb +367 -0
  149. metadata +345 -0
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ class Runo::Checkbox < Runo::Field
7
+
8
+ def initialize(meta = {})
9
+ if meta[:tokens]
10
+ meta[:options] ||= meta[:tokens] - ['mandatory']
11
+ meta[:options] = ['_on'] if meta[:options].empty?
12
+ meta[:mandatory] = meta[:tokens].include?('mandatory') && Array(meta[:options]).size > 1
13
+ end
14
+ if meta[:options].size == 1 && meta[:default] =~ /^(on|true|yes)$/i
15
+ meta[:default] = meta[:options].first
16
+ end
17
+ super
18
+ end
19
+
20
+ def errors
21
+ if val.empty?
22
+ my[:mandatory] ? [_ 'mandatory'] : []
23
+ else
24
+ (val - my[:options]).empty? ? [] : [_ 'no such option']
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def _g_default(arg)
31
+ val.join ', '
32
+ end
33
+
34
+ def _g_update(arg)
35
+ if my[:options] == ['_on']
36
+ checked = (val.include? '_on') ? ' checked' : ''
37
+ <<_html
38
+ <span class="#{_g_class arg}">
39
+ <input type="hidden" name="#{my[:short_name]}[]" value="" />
40
+ <input type="checkbox" name="#{my[:short_name]}[]" value="_on" #{checked}/>
41
+ #{_g_errors arg}</span>
42
+ _html
43
+ else
44
+ options = my[:options].collect {|opt|
45
+ checked = (val.include? opt) ? ' checked' : ''
46
+ <<_html
47
+ <span class="item">
48
+ <input type="checkbox" id="checkbox_#{my[:short_name]}-#{opt}" name="#{my[:short_name]}[]" value="#{opt}"#{checked} />
49
+ <label for="checkbox_#{my[:short_name]}-#{opt}">#{opt}</label>
50
+ </span>
51
+ _html
52
+ }.join
53
+ <<_html
54
+ <span class="#{_g_class arg}">
55
+ <input type="hidden" name="#{my[:short_name]}[]" value="" />
56
+ #{options}#{_g_errors arg}</span>
57
+ _html
58
+ end
59
+ end
60
+ alias :_g_create :_g_update
61
+
62
+ def val_cast(v)
63
+ Array(v).collect {|i|
64
+ i.to_s unless i.to_s.empty?
65
+ }.compact
66
+ end
67
+
68
+ end
@@ -0,0 +1,144 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ class Runo::File < Runo::Field
7
+
8
+ def initialize(meta = {})
9
+ meta[:options].collect! {|i| i.downcase } if meta[:options]
10
+ meta[:size] = $&.to_i if meta[:tokens] && meta[:tokens].first =~ /^\d+$/
11
+ super
12
+ end
13
+
14
+ def meta_path
15
+ my[:full_name].gsub('-', '/')
16
+ end
17
+
18
+ def meta_tmp_path
19
+ "#{Runo.base[:path]}/#{Runo.base[:tid]}/#{my[:short_name].gsub('-', '/')}" if Runo.base
20
+ end
21
+
22
+ def meta_persistent_sd
23
+ find_ancestor {|f| f.is_a?(Runo::Set::Dynamic) && f.storage.persistent? }
24
+ end
25
+
26
+ def meta_persistent_name
27
+ f = my[:parent]
28
+ ps = my[:persistent_sd]
29
+ name = my[:id]
30
+ until f == ps
31
+ name = "#{f[:id]}-#{name}"
32
+ f = f[:parent]
33
+ end
34
+ name
35
+ end
36
+
37
+ def body
38
+ raise Runo::Error::Forbidden unless permit? :read
39
+
40
+ if ps = my[:persistent_sd]
41
+ @body ||= ps.storage.val my[:persistent_name]
42
+ end
43
+ @body
44
+ end
45
+
46
+ def errors
47
+ if (
48
+ val['basename'] &&
49
+ my[:options].is_a?(::Array) &&
50
+ !my[:options].empty? &&
51
+ !my[:options].include?(val['basename'].to_s[/\.([\w\.]+)$/i, 1].downcase)
52
+ )
53
+ [_('wrong file type: should be %{types}') % {:types => my[:options].join('/')}]
54
+ elsif (my[:max].to_i > 0) && (val['size'].to_i > my[:max])
55
+ [_('too large: %{max} bytes maximum') % {:max => my[:max]}]
56
+ elsif (my[:min].to_i == 1) && val['size'].to_i < 1
57
+ [_ 'mandatory']
58
+ elsif (my[:min].to_i > 0) && (val['size'].to_i < my[:min])
59
+ [_('too small: %{min} bytes minimum') % {:min => my[:min]}]
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def commit(type = :temp)
66
+ if type == :temp && @action == :delete
67
+ @val = {}
68
+ @body = nil
69
+ elsif type == :persistent && ps = my[:persistent_sd]
70
+ case @action
71
+ when :create, :update, nil
72
+ ps.storage.store(
73
+ my[:persistent_name],
74
+ @body,
75
+ val['basename'][/\.([\w\.]+)$/, 1] || 'bin'
76
+ ) if @body && valid?
77
+ when :delete
78
+ ps.storage.delete my[:persistent_name]
79
+ end
80
+ end
81
+ super
82
+ end
83
+
84
+ private
85
+
86
+ def _g_default(arg = {})
87
+ path = _path arg[:action]
88
+ basename = Runo::Field.h val['basename']
89
+ type = Runo::Field.h val['type']
90
+ <<_html.chomp unless val.empty?
91
+ <a href="#{path}/#{basename}">#{basename} (#{val['size']} bytes)</a>
92
+ _html
93
+ end
94
+
95
+ def _g_update(arg)
96
+ hidden = <<_html if my[:min].to_i > 0 && val.empty?
97
+ <input type="hidden" name="#{my[:short_name]}" value="" />
98
+ _html
99
+ if (
100
+ !val.empty? &&
101
+ my[:min].to_i == 0 &&
102
+ my[:parent].is_a?(Runo::Set::Static) &&
103
+ my[:parent][:item].find {|id, meta| id != my[:id] && meta[:klass] !~ /^meta-/ }
104
+ )
105
+ delete = <<_html
106
+ <input type="submit" name="#{my[:short_name]}.action-delete" value="#{_ 'delete'}" />
107
+ _html
108
+ end
109
+ update = <<_html
110
+ <input type="file" name="#{my[:short_name]}" size="#{my[:size]}" class="file" />
111
+ _html
112
+ <<_html
113
+ <span class="#{_g_class arg}">
114
+ #{_g_default arg}
115
+ #{hidden}#{update}#{delete}#{_g_errors arg}</span>
116
+ _html
117
+ end
118
+ alias :_g_create :_g_update
119
+
120
+ def _path(action)
121
+ Runo.uri.to_s + ([:read, nil].include?(action) ? my[:path] : my[:tmp_path])
122
+ end
123
+
124
+ def val_cast(v)
125
+ if v && v[:tempfile]
126
+ v[:tempfile].rewind
127
+ @body = v[:tempfile].read
128
+ {
129
+ 'basename' => File.basename(v[:filename]),
130
+ 'type' => v[:type] || 'application/octet-stream',
131
+ 'size' => @body.size,
132
+ }
133
+ elsif v.is_a?(::Hash) && v['basename']
134
+ {
135
+ 'basename' => v['basename'].to_s,
136
+ 'type' => v['type'].to_s,
137
+ 'size' => v['size'].to_i,
138
+ }
139
+ else
140
+ {}
141
+ end
142
+ end
143
+
144
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009-2010 Akira FUNAI
5
+
6
+ begin
7
+ require 'quick_magick'
8
+ rescue LoadError
9
+ end
10
+
11
+ class Runo::Img < Runo::File
12
+
13
+ DEFAULT_META = {
14
+ :width => 120,
15
+ :height => 120,
16
+ :options => ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bmp'],
17
+ }
18
+
19
+ def self.quick_magick?
20
+ Object.const_defined? :QuickMagick
21
+ end
22
+
23
+ def initialize(meta = {})
24
+ meta[:crop] = true if meta[:tokens] && meta[:tokens].include?('crop')
25
+ super
26
+ end
27
+
28
+ def thumbnail
29
+ raise Runo::Error::Forbidden unless permit? :read
30
+
31
+ if ps = my[:persistent_sd]
32
+ @thumbnail ||= ps.storage.val "#{my[:persistent_name]}_small"
33
+ end
34
+ @thumbnail || body
35
+ end
36
+
37
+ def errors
38
+ if @error_thumbnail
39
+ [_('wrong file type: should be %{types}') % {:types => my[:options].join('/')}]
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ def commit(type = :temp)
46
+ super
47
+ if type == :temp && @action == :delete
48
+ @thumbnail = nil
49
+ elsif type == :persistent && ps = my[:persistent_sd]
50
+ case @action
51
+ when :create, :update, nil
52
+ ps.storage.store(
53
+ "#{my[:persistent_name]}_small",
54
+ @thumbnail,
55
+ val['basename'][/\.([\w\.]+)$/, 1] || 'bin'
56
+ ) if @thumbnail && valid?
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def _g_default(arg = {})
64
+ path = _path arg[:action]
65
+ basename = Runo::Field.h val['basename']
66
+ s_basename = basename.sub(/\..+$/, '_small\\&')
67
+ if val.empty?
68
+ <<_html.chomp
69
+ <span class="dummy_img" style="width: #{my[:width]}px; height: #{my[:height]}px;"></span>
70
+ _html
71
+ elsif errors.include?(_('wrong file type: should be %{types}') % {:types => my[:options].join('/')})
72
+ super
73
+ elsif arg[:sub_action] == :without_link
74
+ <<_html.chomp
75
+ <img src="#{path}/#{s_basename}" alt="#{basename}" />
76
+ _html
77
+ else
78
+ <<_html.chomp
79
+ <a href="#{path}/#{basename}"><img src="#{path}/#{s_basename}" alt="#{basename}" /></a>
80
+ _html
81
+ end
82
+ end
83
+
84
+ def _g_thumbnail(arg = {})
85
+ _g_default(arg.merge :sub_action => :without_link)
86
+ end
87
+
88
+ def _thumbnail(tempfile)
89
+ @error_thumbnail = nil
90
+ begin
91
+ tempfile.rewind
92
+ img = QuickMagick::Image.read(tempfile.path).first
93
+ if my[:crop]
94
+ img.gravity = 'center'
95
+ img.resize "#{my[:width]}x#{my[:height]}^"
96
+ img.extent "#{my[:width]}x#{my[:height]}"
97
+ else
98
+ img.resize "#{my[:width]}x#{my[:height]}"
99
+ end
100
+ img.to_blob
101
+ rescue QuickMagick::QuickMagickError
102
+ @error_thumbnail = $!.inspect
103
+ nil
104
+ end if self.class.quick_magick?
105
+ end
106
+
107
+ def val_cast(v)
108
+ @thumbnail = _thumbnail(v[:tempfile]) if v && v[:tempfile]
109
+ super
110
+ end
111
+
112
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ class Runo::Password < Runo::Field
7
+
8
+ def initialize(meta = {})
9
+ meta[:size] = $&.to_i if meta[:tokens] && meta[:tokens].find {|t| t =~ /^\d+$/ }
10
+ super
11
+ end
12
+
13
+ def errors
14
+ if @size.nil?
15
+ []
16
+ elsif (my[:max].to_i > 0) && (@size > my[:max])
17
+ [_('too long: %{max} characters maximum') % {:max => my[:max]}]
18
+ elsif (my[:min].to_i == 1) && (@size == 0)
19
+ [_ 'mandatory']
20
+ elsif (my[:min].to_i > 0) && (@size < my[:min])
21
+ [_('too short: %{min} characters minimum') % {:min => my[:min]}]
22
+ else
23
+ []
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def _g_default(arg)
30
+ '*' * (@size || 5)
31
+ end
32
+
33
+ def _g_update(arg)
34
+ <<_html.chomp
35
+ <span class="#{_g_class arg}"><input type="password" name="#{my[:short_name]}" value="" size="#{my[:size]}" />#{_g_errors arg}</span>
36
+ _html
37
+ end
38
+ alias :_g_create :_g_update
39
+
40
+ def _post(action, v)
41
+ case action
42
+ when :load
43
+ @size = nil
44
+ @val = v
45
+ when :create, :update
46
+ if v.is_a?(::String) && !v.empty?
47
+ salt = ('a'..'z').to_a[rand 26] + ('a'..'z').to_a[rand 26]
48
+ @size = v.size
49
+ @val = v.crypt salt
50
+ elsif @val.nil?
51
+ @size = 0
52
+ else
53
+ # no action: keep current @val
54
+ end
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ class Runo::Radio < Runo::Field
7
+
8
+ def initialize(meta = {})
9
+ meta[:mandatory] = meta[:tokens] && meta[:tokens].include?('mandatory')
10
+ meta[:options] ||= meta[:max] && (meta[:min].to_i..meta[:max]).collect {|i| i.to_s }
11
+ super
12
+ end
13
+
14
+ def errors
15
+ if val.empty?
16
+ my[:mandatory] ? [_ 'mandatory'] : []
17
+ else
18
+ my[:options].include?(val) ? [] : [_ 'no such option']
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def _g_update(arg)
25
+ options = my[:options].collect {|opt|
26
+ checked = (opt == val) ? ' checked' : ''
27
+ h_opt = Runo::Field.h opt
28
+ <<_html
29
+ <span class="item">
30
+ <input type="radio" id="radio_#{my[:short_name]}-#{h_opt}" name="#{my[:short_name]}" value="#{h_opt}"#{checked} />
31
+ <label for="radio_#{my[:short_name]}-#{h_opt}">#{h_opt}</label>
32
+ </span>
33
+ _html
34
+ }.join
35
+ <<_html
36
+ <span class="#{_g_class arg}">
37
+ <input type="hidden" name="#{my[:short_name]}" value="" />
38
+ #{options}#{_g_errors arg}</span>
39
+ _html
40
+ end
41
+ alias :_g_create :_g_update
42
+
43
+ def val_cast(v)
44
+ v.to_s
45
+ end
46
+
47
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ class Runo::Select < Runo::Field
7
+
8
+ def initialize(meta = {})
9
+ meta[:mandatory] = meta[:tokens] && meta[:tokens].include?('mandatory')
10
+ meta[:options] ||= meta[:max] && (meta[:min].to_i..meta[:max]).collect {|i| i.to_s }
11
+ super
12
+ end
13
+
14
+ def errors
15
+ if my[:mandatory] && val.empty?
16
+ [_ 'mandatory']
17
+ elsif my[:options].include?(val) || val.empty?
18
+ []
19
+ else
20
+ [_ 'no such option']
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def _g_update(arg)
27
+ options = my[:options].collect {|opt|
28
+ selected = (opt == val) ? ' selected' : ''
29
+ " <option#{selected}>#{Runo::Field.h opt}</option>\n"
30
+ }.join
31
+ unless my[:mandatory] && my[:options].include?(val)
32
+ options = " <option value=\"\">#{_ 'please select'}</option>\n#{options}"
33
+ end
34
+ <<_html
35
+ <span class="#{_g_class arg}">
36
+ <select name="#{my[:short_name]}">
37
+ #{options} </select>
38
+ #{_g_errors arg}</span>
39
+ _html
40
+ end
41
+ alias :_g_create :_g_update
42
+
43
+ def val_cast(v)
44
+ v.to_s
45
+ end
46
+
47
+ end