aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-03-06 12:43:23 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-03-06 12:43:23 +0000
commit838ae35d63c34872d46bee8b006796ebdd9c7722 (patch)
treec0bc6c776a0a8525017a39bd807be4fd8e09c2bc
parent12c775f20c6d4d2562c99b503f720466e335e411 (diff)
downloadrails-838ae35d63c34872d46bee8b006796ebdd9c7722.tar.gz
rails-838ae35d63c34872d46bee8b006796ebdd9c7722.tar.bz2
rails-838ae35d63c34872d46bee8b006796ebdd9c7722.zip
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
-rw-r--r--activerecord/CHANGELOG16
-rwxr-xr-xactiverecord/lib/active_record/validations.rb118
-rwxr-xr-xactiverecord/test/validations_test.rb38
3 files changed, 130 insertions, 42 deletions
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 <tt>integer</tt> is false) or applying it to the regular expression
+ <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true).
+
+ class Person < ActiveRecord::Base
+ validates_numericality_of :value, :on => :create
+ end
+
+ Configuration options:
+ * <tt>message</tt> - A custom error message (default is: "is not a number")
+ * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
+ * <tt>only_integer</tt> 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:
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
+ # * <tt>allow_nil</tt> - 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:
- # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
- # * <tt>allow_nil</tt> - 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 <tt>integer</tt> is false) or applying it to the regular expression
+ # <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true).
+ #
+ # class Person < ActiveRecord::Base
+ # validates_numericality_of :value, :on => :create
+ # end
+ #
+ # Configuration options:
+ # * <tt>message</tt> - A custom error message (default is: "is not a number")
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
+ # * <tt>only_integer</tt> 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