Added the :if option to all validations that can either use a block or a...

Added the :if option to all validations that can either use a block or a method pointer to determine whether the validation should be run or not. #1324 [Duane Johnson/jhosteny]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1340 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
上级 2bccdba9
*SVN* *SVN*
* Added the :if option to all validations that can either use a block or a method pointer to determine whether the validation should be run or not. #1324 [Duane Johnson/jhosteny]. Examples:
Conditional validations such as the following are made possible:
validates_numericality_of :income, :if => :employed?
Conditional validations can also solve the salted login generator problem:
validates_confirmation_of :password, :if => :new_password?
Using blocks:
validates_presence_of :username, :if => Proc.new { |user| user.signup_step > 1 }
* Fixed use of construct_finder_sql when using :join #1288 [dwlt@dwlt.net] * Fixed use of construct_finder_sql when using :join #1288 [dwlt@dwlt.net]
* Fixed that :delete_sql in has_and_belongs_to_many associations couldn't access record properties #1299 [Rick Olson] * Fixed that :delete_sql in has_and_belongs_to_many associations couldn't access record properties #1299 [Rick Olson]
......
...@@ -227,6 +227,29 @@ def validate_on_update(*methods, &block) ...@@ -227,6 +227,29 @@ def validate_on_update(*methods, &block)
write_inheritable_set(:validate_on_update, methods) write_inheritable_set(:validate_on_update, methods)
end end
def condition_block?(condition)
condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1)
end
# Determine from the given condition (whether a block, procedure, method or string)
# whether or not to validate the record. See #validates_each.
def evaluate_condition(condition, record)
case condition
when Symbol: record.send(condition)
when String: eval(condition, binding)
else
if condition_block?(condition)
condition.call(record)
else
raise(
ActiveRecordError,
"Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
"class implementing a static validation method"
)
end
end
end
# Validates each attribute against a block. # Validates each attribute against a block.
# #
# class Person < ActiveRecord::Base # class Person < ActiveRecord::Base
...@@ -238,12 +261,17 @@ def validate_on_update(*methods, &block) ...@@ -238,12 +261,17 @@ def validate_on_update(*methods, &block)
# Options: # Options:
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update) # * <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. # * <tt>allow_nil</tt> - Skip validation if attribute is nil.
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_each(*attrs) def validates_each(*attrs)
options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {} options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
attrs = attrs.flatten attrs = attrs.flatten
# Declare the validation. # Declare the validation.
send(validation_method(options[:on] || :save)) do |record| send(validation_method(options[:on] || :save)) do |record|
# Don't validate when there is an :if condition and that condition is false
unless options[:if] && !evaluate_condition(options[:if], record)
attrs.each do |attr| attrs.each do |attr|
value = record.send(attr) value = record.send(attr)
next if value.nil? && options[:allow_nil] next if value.nil? && options[:allow_nil]
...@@ -251,6 +279,7 @@ def validates_each(*attrs) ...@@ -251,6 +279,7 @@ def validates_each(*attrs)
end end
end end
end end
end
# Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
# #
...@@ -271,6 +300,9 @@ def validates_each(*attrs) ...@@ -271,6 +300,9 @@ def validates_each(*attrs)
# Configuration options: # Configuration options:
# * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation") # * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update) # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_confirmation_of(*attr_names) def validates_confirmation_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -297,6 +329,9 @@ def validates_confirmation_of(*attr_names) ...@@ -297,6 +329,9 @@ def validates_confirmation_of(*attr_names)
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update) # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which # * <tt>accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
# makes it easy to relate to an HTML checkbox. # makes it easy to relate to an HTML checkbox.
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_acceptance_of(*attr_names) def validates_acceptance_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -313,6 +348,9 @@ def validates_acceptance_of(*attr_names) ...@@ -313,6 +348,9 @@ def validates_acceptance_of(*attr_names)
# Configuration options: # Configuration options:
# * <tt>message</tt> - A custom error message (default is: "has already been taken") # * <tt>message</tt> - A custom error message (default is: "has already been taken")
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update) # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_presence_of(*attr_names) def validates_presence_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:empty], :on => :save } configuration = { :message => ActiveRecord::Errors.default_error_messages[:empty], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -321,10 +359,12 @@ def validates_presence_of(*attr_names) ...@@ -321,10 +359,12 @@ def validates_presence_of(*attr_names)
# while errors.add_on_empty can # while errors.add_on_empty can
attr_names.each do |attr_name| attr_names.each do |attr_name|
send(validation_method(configuration[:on])) do |record| send(validation_method(configuration[:on])) do |record|
unless configuration[:if] and not evaluate_condition(configuration[:if], record)
record.errors.add_on_empty(attr_name,configuration[:message]) record.errors.add_on_empty(attr_name,configuration[:message])
end end
end end
end end
end
# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
# #
...@@ -350,6 +390,9 @@ def validates_presence_of(*attr_names) ...@@ -350,6 +390,9 @@ def validates_presence_of(*attr_names)
# * <tt>wrong_length</tt> - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)") # * <tt>wrong_length</tt> - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)")
# * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message # * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update) # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_length_of(*attrs) def validates_length_of(*attrs)
# Merge given options with defaults. # Merge given options with defaults.
options = DEFAULT_SIZE_VALIDATION_OPTIONS.dup options = DEFAULT_SIZE_VALIDATION_OPTIONS.dup
...@@ -408,6 +451,9 @@ def validates_length_of(*attrs) ...@@ -408,6 +451,9 @@ def validates_length_of(*attrs)
# Configuration options: # Configuration options:
# * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken") # * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
# * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope" # * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_uniqueness_of(*attr_names) def validates_uniqueness_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] } configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -436,6 +482,9 @@ def validates_uniqueness_of(*attr_names) ...@@ -436,6 +482,9 @@ def validates_uniqueness_of(*attr_names)
# * <tt>message</tt> - A custom error message (default is: "is invalid") # * <tt>message</tt> - A custom error message (default is: "is invalid")
# * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!) # * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!)
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update) # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names) def validates_format_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -458,6 +507,9 @@ def validates_format_of(*attr_names) ...@@ -458,6 +507,9 @@ def validates_format_of(*attr_names)
# * <tt>in</tt> - An enumerable object of available items # * <tt>in</tt> - An enumerable object of available items
# * <tt>message</tt> - Specifies a customer error message (default is: "is not included in the list") # * <tt>message</tt> - Specifies a customer error message (default is: "is not included in the list")
# * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false) # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_inclusion_of(*attr_names) def validates_inclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -482,6 +534,9 @@ def validates_inclusion_of(*attr_names) ...@@ -482,6 +534,9 @@ def validates_inclusion_of(*attr_names)
# * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of # * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of
# * <tt>message</tt> - Specifies a customer error message (default is: "is reserved") # * <tt>message</tt> - Specifies a customer error message (default is: "is reserved")
# * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false) # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_exclusion_of(*attr_names) def validates_exclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -516,6 +571,9 @@ def validates_exclusion_of(*attr_names) ...@@ -516,6 +571,9 @@ def validates_exclusion_of(*attr_names)
# #
# Configuration options: # Configuration options:
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update) # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names) def validates_associated(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
...@@ -539,6 +597,9 @@ def validates_associated(*attr_names) ...@@ -539,6 +597,9 @@ def validates_associated(*attr_names)
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update) # * <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) # * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
# * <tt>allow_nil</tt> Skip validation if attribute is nil (default is false). Notice that for fixnum and float columsn empty strings are converted to nil # * <tt>allow_nil</tt> Skip validation if attribute is nil (default is false). Notice that for fixnum and float columsn empty strings are converted to nil
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_numericality_of(*attr_names) def validates_numericality_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save, configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save,
:only_integer => false, :allow_nil => false } :only_integer => false, :allow_nil => false }
......
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
require 'fixtures/reply' require 'fixtures/reply'
require 'fixtures/developer' require 'fixtures/developer'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
def condition_is_true
return true
end
def condition_is_true_but_its_not
return false
end
end
class ValidationsTest < Test::Unit::TestCase class ValidationsTest < Test::Unit::TestCase
fixtures :topics, :developers fixtures :topics, :developers
...@@ -707,4 +718,56 @@ def test_validates_numericality_of_int ...@@ -707,4 +718,56 @@ def test_validates_numericality_of_int
end end
end end
def test_conditional_validation_using_method_true
# When the method returns true
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert !t.valid?
assert t.errors.on(:title)
assert_equal "hoo 5", t.errors["title"]
end
def test_conditional_validation_using_method_false
# When the method returns false
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true_but_its_not )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert !t.errors.on(:title)
end
def test_conditional_validation_using_string_true
# When the evaluated string returns true
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "a = 1; a == 1" )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert !t.valid?
assert t.errors.on(:title)
assert_equal "hoo 5", t.errors["title"]
end
def test_conditional_validation_using_string_false
# When the evaluated string returns false
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "false")
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert !t.errors.on(:title)
end
def test_conditional_validation_using_block_true
# When the block returns true
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
:if => Proc.new { |r| r.content.size > 4 } )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert !t.valid?
assert t.errors.on(:title)
assert_equal "hoo 5", t.errors["title"]
end
def test_conditional_validation_using_block_false
# When the block returns false
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
:if => Proc.new { |r| r.title != "uhohuhoh"} )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert !t.errors.on(:title)
end
end end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册