From 838ae35d63c34872d46bee8b006796ebdd9c7722 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 6 Mar 2005 12:43:23 +0000 Subject: Added validates_numericality_of #716 [skanthak/c.r.mcgrath] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@842 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 16 ++++ activerecord/lib/active_record/validations.rb | 118 +++++++++++++++++--------- activerecord/test/validations_test.rb | 38 +++++++++ 3 files changed, 130 insertions(+), 42 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 96ba906434..d17ff99f54 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,21 @@ *SVN* +* Added validates_numericality_of #716 [skanthak/c.r.mcgrath]. Docuemntation: + + Validates whether the value of the specified attribute is numeric by trying to convert it to + a float with Kernel.Float (if integer is false) or applying it to the regular expression + /^[\+\-]?\d+$/ (if integer is set to true). + + class Person < ActiveRecord::Base + validates_numericality_of :value, :on => :create + end + + Configuration options: + * message - A custom error message (default is: "is not a number") + * on Specifies when this validation is active (default is :save, other options :create, :update) + * only_integer Specifies whether the value has to be an integer, e.g. an integral value (default is false) + + * Fixed that HasManyAssociation#count was using :finder_sql rather than :counter_sql if it was available #445 [Scott Barron] * Added better defaults for composed_of, so statements like composed_of :time_zone, :mapping => %w( time_zone time_zone ) can be written without the mapping part (it's now assumed) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 520ebd8458..abce28a4ec 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -16,6 +16,7 @@ module ActiveRecord :too_short => "is too short (min is %d characters)", :wrong_length => "is the wrong length (should be %d characters)", :taken => "has already been taken", + :not_a_number => "is not a number", } # Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations. @@ -193,6 +194,20 @@ module ActiveRecord # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use # these over the low-level calls to validate and validate_on_create when possible. module ClassMethods + DEFAULT_VALIDATION_OPTIONS = { + :on => :save, + :allow_nil => false, + :message => nil + }.freeze + + DEFAULT_SIZE_VALIDATION_OPTIONS = DEFAULT_VALIDATION_OPTIONS.merge( + :too_long => ActiveRecord::Errors.default_error_messages[:too_long], + :too_short => ActiveRecord::Errors.default_error_messages[:too_short], + :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] + ).freeze + + ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze + def validate(*methods, &block) methods << block if block_given? write_inheritable_set(:validate, methods) @@ -208,6 +223,31 @@ module ActiveRecord write_inheritable_set(:validate_on_update, methods) end + # Validates each attribute against a block. + # + # class Person < ActiveRecord::Base + # validates_each :first_name, :last_name do |record, attr| + # record.errors.add attr, 'starts with z.' if attr[0] == ?z + # end + # end + # + # Options: + # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * allow_nil - Skip validation if attribute is nil. + def validates_each(*attrs) + options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {} + attrs = attrs.flatten + + # Declare the validation. + send(validation_method(options[:on] || :save)) do |record| + attrs.each do |attr| + value = record.send(attr) + next if value.nil? && options[:allow_nil] + yield record, attr, value + end + end + end + # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: # # Model: @@ -279,48 +319,6 @@ module ActiveRecord end end - - DEFAULT_VALIDATION_OPTIONS = { - :on => :save, - :allow_nil => false, - :message => nil - }.freeze - - DEFAULT_SIZE_VALIDATION_OPTIONS = DEFAULT_VALIDATION_OPTIONS.merge( - :too_long => ActiveRecord::Errors.default_error_messages[:too_long], - :too_short => ActiveRecord::Errors.default_error_messages[:too_short], - :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] - ).freeze - - ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze - - - # Validates each attribute against a block. - # - # class Person < ActiveRecord::Base - # validates_each :first_name, :last_name do |record, attr| - # record.errors.add attr, 'starts with z.' if attr[0] == ?z - # end - # end - # - # Options: - # * on - Specifies when this validation is active (default is :save, other options :create, :update) - # * allow_nil - Skip validation if attribute is nil. - def validates_each(*attrs) - options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {} - attrs = attrs.flatten - - # Declare the validation. - send(validation_method(options[:on] || :save)) do |record| - attrs.each do |attr| - value = record.send(attr) - next if value.nil? && options[:allow_nil] - yield record, attr, value - end - end - end - - # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # # class Person < ActiveRecord::Base @@ -516,6 +514,42 @@ module ActiveRecord end end + # Validates whether the value of the specified attribute is numeric by trying to convert it to + # a float with Kernel.Float (if integer is false) or applying it to the regular expression + # /^[\+\-]?\d+$/ (if integer is set to true). + # + # class Person < ActiveRecord::Base + # validates_numericality_of :value, :on => :create + # end + # + # Configuration options: + # * message - A custom error message (default is: "is not a number") + # * on Specifies when this validation is active (default is :save, other options :create, :update) + # * only_integer Specifies whether the value has to be an integer, e.g. an integral value (default is false) + def validates_numericality_of(*attr_names) + configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save, + :integer => false } + configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) + + for attr_name in attr_names + if configuration[:only_integer] + # we have to use a regexp here, because Kernel.Integer accepts nil and "0xdeadbeef", but does not + # accept "099" and String#to_i accepts everything. The string containing the regexp is evaluated twice + # so we have to escape everything properly + class_eval(%(#{validation_method(configuration[:on])} %{ + errors.add("#{attr_name}", "#{configuration[:message]}") unless #{attr_name}_before_type_cast.to_s =~ /^[\\\\+\\\\-]?\\\\d+$/ + })) + else + class_eval(%(#{validation_method(configuration[:on])} %{ + begin + Kernel.Float(#{attr_name}_before_type_cast) + rescue ArgumentError, TypeError + errors.add("#{attr_name}", "#{configuration[:message]}") + end + })) + end + end + end private def write_inheritable_set(key, methods) diff --git a/activerecord/test/validations_test.rb b/activerecord/test/validations_test.rb index 9d7b8b987a..913c72ea3c 100755 --- a/activerecord/test/validations_test.rb +++ b/activerecord/test/validations_test.rb @@ -604,4 +604,42 @@ class ValidationsTest < Test::Unit::TestCase assert !r.valid? assert_equal r.errors.on(:topic).first, "This string contains 'single' and \"double\" quotes" end + + def test_validates_numericality_of_with_string + Topic.validates_numericality_of( :replies_count ) + ["not a number","42 not a number","0xdeadbeef","00-1","-+019.0","12.12.13.12",nil].each do |v| + t = Topic.create("title" => "numeric test", "content" => "whatever", "replies_count" => "not a number") + assert !t.valid?, "#{v} not rejected as a number" + assert t.errors.on(:replies_count) + end + end + + def test_validates_numericality_of + Topic.validates_numericality_of( :replies_count ) + ["10", "10.0", "10.5", "-10.5", "-0.0001","0090","-090","-090.1"].each do |v| + t = Topic.create("title" => "numeric test", "content" => "whatever", "replies_count" => v) + assert t.valid?, "#{v} not recognized as a number" + # we cannot check this as replies_count is actually an integer field + #assert_in_delta v.to_f, t.replies_count, 0.0000001 + end + end + + def test_validates_numericality_of_int_with_string + Topic.validates_numericality_of( :replies_count, :only_integer => true ) + ["not a number","42 not a number","0xdeadbeef","0-1","--3","+-3","+3-1",nil].each do |v| + t = Topic.create("title" => "numeric test", "content" => "whatever", "replies_count" => v) + assert !t.valid?, "#{v} not rejected as integer" + assert t.errors.on(:replies_count) + end + end + + def test_validates_numericality_of_int + Topic.validates_numericality_of( :replies_count, :only_integer => true ) + ["42", "+42", "-42", "042", "0042", "-042", 42].each do |v| + t = Topic.create("title" => "numeric test", "content" => "whatever", "replies_count" => v) + assert t.valid?, "#{v} not recognized as integer" + assert_equal v.to_i, t.replies_count + end + end + end -- cgit v1.2.3