amount_field_rails3 3.0.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.
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/History.txt +58 -0
- data/MIT-LICENSE +20 -0
- data/Manifest +21 -0
- data/README.rdoc +196 -0
- data/Rakefile +16 -0
- data/amount_field.gemspec +25 -0
- data/init.rb +4 -0
- data/install.rb +1 -0
- data/lib/amount_field.rb +23 -0
- data/lib/amount_field/configuration.rb +20 -0
- data/lib/amount_field/form_helper.rb +68 -0
- data/lib/amount_field/form_tag_helper.rb +22 -0
- data/lib/amount_field/validations.rb +129 -0
- data/lib/amount_field/version.rb +3 -0
- data/locale/de.yml +10 -0
- data/locale/en.yml +11 -0
- data/test/form_helper_test.rb +264 -0
- data/test/form_tag_helper_test.rb +47 -0
- data/test/models/test_product.rb +7 -0
- data/test/test_helper.rb +63 -0
- data/test/validations_test.rb +389 -0
- data/uninstall.rb +1 -0
- metadata +104 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module AmountField #:nodoc:
|
2
|
+
module Helpers #:nodoc:
|
3
|
+
module FormTagHelper
|
4
|
+
include ActionView::Helpers::NumberHelper
|
5
|
+
|
6
|
+
def amount_field_tag(name, value = nil, options = {})
|
7
|
+
format_options = I18n.t(:'number.amount_field.format', :raise => true) rescue {}
|
8
|
+
format_options = format_options.merge(AmountField::ActiveRecord::Validations.configuration)
|
9
|
+
format_options.merge!(options.delete(:format) || {})
|
10
|
+
|
11
|
+
# if no explicit value is given, we set a formatted one. In case of an error we take the
|
12
|
+
# original value inserted by the user.
|
13
|
+
options[:value] ||= number_with_precision(value.to_s, format_options)
|
14
|
+
options[:name] = "#{AmountField::Configuration.prefix}_#{name}"
|
15
|
+
options[:class] = "#{options[:class]} #{AmountField::Configuration.css_class}"
|
16
|
+
|
17
|
+
text_field_tag(name, value, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module AmountField #:nodoc:
|
2
|
+
module ActiveRecord #:nodoc:
|
3
|
+
module Validations
|
4
|
+
|
5
|
+
@@configuration = {}
|
6
|
+
mattr_accessor :configuration
|
7
|
+
|
8
|
+
def self.included(base) # :nodoc:
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def validates_amount_format_of(*attr_names)
|
15
|
+
# code before validates_each is called only once!
|
16
|
+
configuration = attr_names.extract_options!
|
17
|
+
|
18
|
+
define_special_setter(attr_names, configuration)
|
19
|
+
|
20
|
+
# the following code defines the callbacks methods that are called on every validation
|
21
|
+
validates_each(attr_names, configuration) do |record, attr_name, value|
|
22
|
+
# in case there is no assignment via 'amount_field_XXX=' method, we don't need to validate.
|
23
|
+
next unless record.instance_variable_defined?("@#{special_method_name(attr_name)}")
|
24
|
+
|
25
|
+
# get the original assigned value first to always run the validation for this value!
|
26
|
+
# if we us 'before_type_cast' here, we would get the converted value and not the
|
27
|
+
# original value if we call the validation twice.
|
28
|
+
original_value = record.instance_variable_get("@#{special_method_name(attr_name)}")
|
29
|
+
original_value ||= record.send("#{attr_name}_before_type_cast") || value
|
30
|
+
|
31
|
+
# in case nil or blank is allowed, we don't validate
|
32
|
+
next if configuration[:allow_nil] and original_value.nil?
|
33
|
+
next if configuration[:allow_blank] and original_value.blank?
|
34
|
+
|
35
|
+
converted_value = convert(original_value, format_configuration(configuration))
|
36
|
+
|
37
|
+
if valid_format?(original_value, format_configuration(configuration))
|
38
|
+
# assign converted value to attribute so other validations macro can work on it
|
39
|
+
# and the getter returns a value
|
40
|
+
record.send("#{attr_name}=", converted_value)
|
41
|
+
else
|
42
|
+
# assign original value as AssignedValue so multiple calls of this validation will
|
43
|
+
# consider the value still as invalid.
|
44
|
+
record.send("#{attr_name}=", original_value)
|
45
|
+
record.errors.add(attr_name,
|
46
|
+
build_error_message(configuration[:message], {
|
47
|
+
:value => original_value,
|
48
|
+
:format_example => valid_format_example(format_configuration(configuration))
|
49
|
+
})
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def define_special_setter(attr_names, configuration)
|
59
|
+
attr_names.each do |attr_name|
|
60
|
+
class_eval <<-EOV
|
61
|
+
def #{special_method_name(attr_name)}=(value)
|
62
|
+
@#{special_method_name(attr_name)} = value
|
63
|
+
self[:#{attr_name}] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def #{special_method_name(attr_name)}
|
67
|
+
@#{special_method_name(attr_name)}
|
68
|
+
end
|
69
|
+
EOV
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def special_method_name(attr_name)
|
74
|
+
"#{AmountField::Configuration.prefix}_#{attr_name}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def convert(original_value, configuration)
|
78
|
+
converted_value = original_value.to_s.gsub(configuration[:delimiter].to_s, '')
|
79
|
+
converted_value = converted_value.sub(configuration[:separator].to_s, '.') unless configuration[:separator].blank?
|
80
|
+
converted_value
|
81
|
+
end
|
82
|
+
|
83
|
+
# we have to read the configuration every time to get the current I18n value
|
84
|
+
def format_configuration(configuration)
|
85
|
+
format_options = I18n.t(:'number.amount_field.format', :raise => true) rescue {}
|
86
|
+
# update it with a maybe given default configuration
|
87
|
+
format_options = format_options.merge(AmountField::ActiveRecord::Validations.configuration)
|
88
|
+
# update it with a maybe given explicit configuration via the macro
|
89
|
+
format_options.update(configuration)
|
90
|
+
end
|
91
|
+
|
92
|
+
def valid_format?(value, configuration)
|
93
|
+
return false if !value.kind_of?(String) || value.blank?
|
94
|
+
|
95
|
+
# add ,00 to 1234
|
96
|
+
if !value.include?(configuration[:separator].to_s) && !configuration[:separator].blank?
|
97
|
+
value = "#{value}#{configuration[:separator]}#{'0' * configuration[:precision].to_i}"
|
98
|
+
end
|
99
|
+
|
100
|
+
cs = configuration[:separator]
|
101
|
+
cd = "\\#{configuration[:delimiter] || ' '}"
|
102
|
+
cp = configuration[:precision]
|
103
|
+
|
104
|
+
# (1234 | 123.456),00
|
105
|
+
!(value =~ /\A[-\+]{0,1}((\d*)|(\d{0,3}(#{cd}\d{3})*))#{cs}\d{0,#{cp}}\z/).nil?
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid_format_example(configuration)
|
109
|
+
s = 'd'
|
110
|
+
s << "#{configuration[:delimiter]}" unless configuration[:delimiter].nil?
|
111
|
+
s << "ddd"
|
112
|
+
s << "#{configuration[:separator]}" unless configuration[:separator].nil?
|
113
|
+
s << "#{'d' * configuration[:precision].to_i}" unless configuration[:precision].nil?
|
114
|
+
s
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_error_message(explicit_message, options = {})
|
118
|
+
if explicit_message.blank?
|
119
|
+
I18n.t('errors.messages.invalid_amount_format', options)
|
120
|
+
else
|
121
|
+
I18n.interpolate(explicit_message, options)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/locale/de.yml
ADDED
data/locale/en.yml
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Taken from the FormHelperTest in Rails 2.3
|
7
|
+
#
|
8
|
+
# We are testing the FormBuilder- and FormHelper-Version at once.
|
9
|
+
#
|
10
|
+
class FormHelperTest < ActionView::TestCase
|
11
|
+
tests AmountField::Helpers::FormHelper
|
12
|
+
|
13
|
+
class MyFormBuilder < ActionView::Helpers::FormBuilder
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup
|
17
|
+
@controller = Class.new do
|
18
|
+
attr_reader :url_for_options
|
19
|
+
def url_for(options)
|
20
|
+
@url_for_options = options
|
21
|
+
"http://www.example.com"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
@controller = @controller.new
|
25
|
+
@test_product = TestProduct.new(:price => 1234.56)
|
26
|
+
|
27
|
+
AmountField::Configuration.prefix = 'amount_field'
|
28
|
+
AmountField::Configuration.css_class = 'amount_field'
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_products_path
|
32
|
+
"/products"
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_product_path
|
36
|
+
"/products"
|
37
|
+
end
|
38
|
+
|
39
|
+
test "amount_field form helper with locale de" do
|
40
|
+
with_locale('de') do
|
41
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
42
|
+
concat f.amount_field(:price)
|
43
|
+
end
|
44
|
+
|
45
|
+
expected_input =
|
46
|
+
"<input class=' amount_field' id='test_product_price'" +
|
47
|
+
" name='test_product[amount_field_price]' size='30' type='text' value='1.234,56' />"
|
48
|
+
|
49
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price)
|
50
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
test "amount_field form helper with locale en" do
|
55
|
+
with_locale('en') do
|
56
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
57
|
+
concat f.amount_field(:price)
|
58
|
+
end
|
59
|
+
|
60
|
+
expected_input =
|
61
|
+
"<input name='test_product[amount_field_price]' size='30' type='text'" +
|
62
|
+
" class=' amount_field' id='test_product_price' value='1,234.56' />"
|
63
|
+
|
64
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price)
|
65
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
test "configured prefix is use in amount_field" do
|
70
|
+
with_locale('de') do
|
71
|
+
AmountField::Configuration.prefix = 'my_prefix'
|
72
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
73
|
+
concat f.amount_field(:price)
|
74
|
+
end
|
75
|
+
|
76
|
+
expected_input =
|
77
|
+
"<input class=' amount_field' name='test_product[my_prefix_price]' size='30' type='text'" +
|
78
|
+
" id='test_product_price' value='1.234,56' />"
|
79
|
+
|
80
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price)
|
81
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
82
|
+
AmountField::Configuration.prefix = 'amount_field'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
test "configured css class is use in amount_field" do
|
87
|
+
with_locale('de') do
|
88
|
+
AmountField::Configuration.css_class = 'my_class'
|
89
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
90
|
+
concat f.amount_field(:price)
|
91
|
+
end
|
92
|
+
|
93
|
+
expected_input =
|
94
|
+
"<input name='test_product[amount_field_price]' size='30' type='text'" +
|
95
|
+
" class=' my_class' id='test_product_price' value='1.234,56' />"
|
96
|
+
|
97
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price)
|
98
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
99
|
+
AmountField::Configuration.css_class = 'amount_field'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
test "default configuration format overwrite I18n configuration" do
|
104
|
+
with_configuration({ :delimiter => '@', :separator => '/', :precision => 2}) do
|
105
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
106
|
+
concat f.amount_field(:price)
|
107
|
+
end
|
108
|
+
|
109
|
+
expected_input =
|
110
|
+
"<input name='test_product[amount_field_price]' size='30' type='text'" +
|
111
|
+
" class=' amount_field' id='test_product_price' value='1@234/56' />"
|
112
|
+
|
113
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price)
|
114
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
test "explicit format overwrite default configuration" do
|
119
|
+
format = { :delimiter => '@', :separator => '/', :precision => 3 }
|
120
|
+
with_locale('de') do
|
121
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
122
|
+
concat f.amount_field(:price, :format => format)
|
123
|
+
end
|
124
|
+
|
125
|
+
expected_input =
|
126
|
+
"<input name='test_product[amount_field_price]' size='30' type='text'" +
|
127
|
+
" class=' amount_field' id='test_product_price' value='1@234/560' />"
|
128
|
+
|
129
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price, :format => format)
|
130
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
131
|
+
assert_equal({}, AmountField::ActiveRecord::Validations.configuration)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
test "we show the original value for an invalid value" do
|
136
|
+
@test_product = TestProduct.new(:amount_field_price => "x")
|
137
|
+
@test_product.valid?
|
138
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
139
|
+
concat f.amount_field(:price)
|
140
|
+
end
|
141
|
+
|
142
|
+
expected_input =
|
143
|
+
"<div class='field_with_errors'>" +
|
144
|
+
"<input name='test_product[amount_field_price]' size='30'" +
|
145
|
+
" class=' amount_field' type='text' id='test_product_price' value='x' />" +
|
146
|
+
"</div>"
|
147
|
+
|
148
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price)
|
149
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
150
|
+
end
|
151
|
+
|
152
|
+
test "we show the given value instead of the invalid value" do
|
153
|
+
@test_product = TestProduct.new(:amount_field_price => "x")
|
154
|
+
@test_product.valid?
|
155
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
156
|
+
concat f.amount_field(:price, :value => 4711)
|
157
|
+
end
|
158
|
+
|
159
|
+
expected_input =
|
160
|
+
"<div class='field_with_errors'>" +
|
161
|
+
"<input name='test_product[amount_field_price]' size='30'" +
|
162
|
+
" class=' amount_field' type='text' id='test_product_price' value='4711' />" +
|
163
|
+
"</div>"
|
164
|
+
|
165
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price, :value => 4711)
|
166
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
167
|
+
end
|
168
|
+
|
169
|
+
test "we use the object from options if given" do
|
170
|
+
@test_product1 = TestProduct.new(:amount_field_price => "6543.21")
|
171
|
+
@test_product1.valid?
|
172
|
+
test_product2 = TestProduct.new(:amount_field_price => "1234.56")
|
173
|
+
test_product2.valid?
|
174
|
+
output_buffer = form_for(@test_product1, :builder => MyFormBuilder) do |f|
|
175
|
+
concat f.amount_field(:price, :object => test_product2)
|
176
|
+
end
|
177
|
+
|
178
|
+
expected_input =
|
179
|
+
"<input name='test_product[amount_field_price]' size='30'" +
|
180
|
+
" class=' amount_field' type='text' id='test_product_price' value='1,234.56' />"
|
181
|
+
|
182
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price, :object => test_product2)
|
183
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
184
|
+
end
|
185
|
+
|
186
|
+
test "consider option name if given" do
|
187
|
+
@test_product = TestProduct.new(:amount_field_price => "47.11")
|
188
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
189
|
+
concat f.amount_field(:price, :name => 'article')
|
190
|
+
end
|
191
|
+
|
192
|
+
expected_input =
|
193
|
+
"<input name='article' size='30'" +
|
194
|
+
" class=' amount_field' type='text' id='test_product_price' value='47.11' />"
|
195
|
+
|
196
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price, :name => 'article', :value => 47.11)
|
197
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
198
|
+
end
|
199
|
+
|
200
|
+
test "consider option id if given" do
|
201
|
+
@test_product = TestProduct.new(:amount_field_price => "63.41")
|
202
|
+
output_buffer = form_for(@test_product, :builder => MyFormBuilder) do |f|
|
203
|
+
concat f.amount_field(:price, :name => 'article', :id => 'my_id')
|
204
|
+
end
|
205
|
+
|
206
|
+
expected_input =
|
207
|
+
"<input name='article' size='30'" +
|
208
|
+
" class=' amount_field' type='text' id='my_id' value='63.41' />"
|
209
|
+
|
210
|
+
assert_dom_equal expected_input, amount_field(:test_product, :price, :name => 'article', :id => 'my_id', :value => 63.41)
|
211
|
+
assert_dom_equal expected_form(expected_input), output_buffer
|
212
|
+
end
|
213
|
+
|
214
|
+
test "invalid negative value is displayed with current format options" do
|
215
|
+
with_locale('de') do
|
216
|
+
class MyTestProduct < ActiveRecord::Base
|
217
|
+
set_table_name 'test_products'
|
218
|
+
validates_amount_format_of :price
|
219
|
+
validates_numericality_of :price, :greater_than_or_equal_to => 0.0
|
220
|
+
end
|
221
|
+
@test_product = MyTestProduct.new(:amount_field_price => "-0,1")
|
222
|
+
@test_product.valid?
|
223
|
+
|
224
|
+
output_buffer = form_for(@test_product, :url => test_products_path, :builder => MyFormBuilder) do |f|
|
225
|
+
concat f.amount_field(:price)
|
226
|
+
end
|
227
|
+
|
228
|
+
assert_match /value="-0,1"/, output_buffer
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
test "invalid positive value is displayed with current format options" do
|
233
|
+
with_locale('de') do
|
234
|
+
class MyTestProduct < ActiveRecord::Base
|
235
|
+
set_table_name 'test_products'
|
236
|
+
validates_amount_format_of :price
|
237
|
+
validates_numericality_of :price, :less_than_or_equal_to => 10.0
|
238
|
+
end
|
239
|
+
@test_product = MyTestProduct.new(:amount_field_price => "12,34")
|
240
|
+
@test_product.valid?
|
241
|
+
|
242
|
+
output_buffer = form_for(@test_product, :url => test_products_path, :builder => MyFormBuilder) do |f|
|
243
|
+
concat f.amount_field(:price)
|
244
|
+
end
|
245
|
+
|
246
|
+
assert_match /value="12,34"/, output_buffer
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
protected
|
251
|
+
|
252
|
+
def protect_against_forgery?
|
253
|
+
false
|
254
|
+
end
|
255
|
+
|
256
|
+
def expected_form(expected_input_field)
|
257
|
+
expected_form =
|
258
|
+
"<form accept-charset='UTF-8' action='/products' class='new_test_product' id='new_test_product' method='post'>" +
|
259
|
+
"<div style='margin:0;padding:0;display:inline'>" +
|
260
|
+
"<input name='utf8' type='hidden' value='✓' /></div>" +
|
261
|
+
"#{expected_input_field}</form>"
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FormTagHelperTest < ActionView::TestCase
|
4
|
+
tests AmountField::Helpers::FormTagHelper
|
5
|
+
|
6
|
+
test "return an input field with the special amount field name attribute" do
|
7
|
+
assert_match /name="amount_field_price"/, amount_field_tag(:price, 1234.56)
|
8
|
+
end
|
9
|
+
|
10
|
+
test "return an value attribute with a formatted value for german locale" do
|
11
|
+
with_locale('de') do
|
12
|
+
assert_match /value="1.234,56"/, amount_field_tag(:price, 1234.56)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
test "return an value attribute with a formatted value for english locale" do
|
17
|
+
with_locale('en') do
|
18
|
+
assert_match /value="1,234.56"/, amount_field_tag(:price, 1234.56)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
test "default css class is used in combination with given class" do
|
23
|
+
assert_match /class="foo #{AmountField::Configuration.css_class}"/, amount_field_tag(:price, 1234.56, :class => 'foo')
|
24
|
+
end
|
25
|
+
|
26
|
+
test "configured prefix is use in amount_field" do
|
27
|
+
AmountField::Configuration.prefix = 'my_prefix'
|
28
|
+
assert_match /name="my_prefix_price"/, amount_field_tag(:price, 1234.56)
|
29
|
+
AmountField::Configuration.prefix = 'amount_field'
|
30
|
+
end
|
31
|
+
|
32
|
+
test "configured css class is use in amount_field" do
|
33
|
+
AmountField::Configuration.css_class = 'my_class'
|
34
|
+
assert_match /class=" my_class"/, amount_field_tag(:price, 1234.56)
|
35
|
+
AmountField::Configuration.css_class = 'amount_field'
|
36
|
+
end
|
37
|
+
|
38
|
+
test "explicit format overwrite default configuration" do
|
39
|
+
assert_match /value="1@234#560"/, amount_field_tag(:price, 1234.56, :format => { :delimiter => '@', :separator => '#', :precision => 3})
|
40
|
+
assert_equal({}, AmountField::ActiveRecord::Validations.configuration)
|
41
|
+
end
|
42
|
+
|
43
|
+
test "we show the given value from the options instead of the argument" do
|
44
|
+
assert_match /value="42"/, amount_field_tag(:price, 1234.56, :value => 42)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|