From 10eb22cdce104326e319826af0ea4292940a9246 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 21 May 2005 10:57:18 +0000 Subject: [PATCH] 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 --- activerecord/CHANGELOG | 11 +++ activerecord/lib/active_record/validations.rb | 71 +++++++++++++++++-- activerecord/test/validations_test.rb | 67 ++++++++++++++++- 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index ee34dab6f8..4a0b0df361 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,16 @@ *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 that :delete_sql in has_and_belongs_to_many associations couldn't access record properties #1299 [Rick Olson] diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 03a71f095b..052484c365 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -227,6 +227,29 @@ def validate_on_update(*methods, &block) write_inheritable_set(:validate_on_update, methods) 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. # # class Person < ActiveRecord::Base @@ -238,16 +261,22 @@ def validate_on_update(*methods, &block) # Options: # * on - Specifies when this validation is active (default is :save, other options :create, :update) # * allow_nil - Skip validation if attribute is nil. + # * if - 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) 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 + # 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| + value = record.send(attr) + next if value.nil? && options[:allow_nil] + yield record, attr, value + end end end end @@ -271,6 +300,9 @@ def validates_each(*attrs) # Configuration options: # * message - A custom error message (default is: "doesn't match confirmation") # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -297,6 +329,9 @@ def validates_confirmation_of(*attr_names) # * on - Specifies when this validation is active (default is :save, other options :create, :update) # * accept - Specifies value that is considered accepted. The default value is a string "1", which # makes it easy to relate to an HTML checkbox. + # * if - 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) 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) @@ -313,6 +348,9 @@ def validates_acceptance_of(*attr_names) # Configuration options: # * message - A custom error message (default is: "has already been taken") # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:empty], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -321,7 +359,9 @@ def validates_presence_of(*attr_names) # while errors.add_on_empty can attr_names.each do |attr_name| send(validation_method(configuration[:on])) do |record| - record.errors.add_on_empty(attr_name,configuration[:message]) + unless configuration[:if] and not evaluate_condition(configuration[:if], record) + record.errors.add_on_empty(attr_name,configuration[:message]) + end end end end @@ -350,6 +390,9 @@ def validates_presence_of(*attr_names) # * wrong_length - 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)") # * message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message # * on - Specifies when this validation is active (default is :save, other options :create, :update) + # * if - 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) # Merge given options with defaults. options = DEFAULT_SIZE_VALIDATION_OPTIONS.dup @@ -408,6 +451,9 @@ def validates_length_of(*attrs) # Configuration options: # * message - Specifies a custom error message (default is: "has already been taken") # * scope - Ensures that the uniqueness is restricted to a condition of "scope = record.scope" + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -436,6 +482,9 @@ def validates_uniqueness_of(*attr_names) # * message - A custom error message (default is: "is invalid") # * with - The regular expression used to validate the format with (note: must be supplied!) # * on Specifies when this validation is active (default is :save, other options :create, :update) + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -458,6 +507,9 @@ def validates_format_of(*attr_names) # * in - An enumerable object of available items # * message - Specifies a customer error message (default is: "is not included in the list") # * allow_nil - If set to true, skips this validation if the attribute is null (default is: false) + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -482,6 +534,9 @@ def validates_inclusion_of(*attr_names) # * in - An enumerable object of items that the value shouldn't be part of # * message - Specifies a customer error message (default is: "is reserved") # * allow_nil - If set to true, skips this validation if the attribute is null (default is: false) + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -516,6 +571,9 @@ def validates_exclusion_of(*attr_names) # # Configuration options: # * on Specifies when this validation is active (default is :save, other options :create, :update) + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) @@ -539,6 +597,9 @@ def validates_associated(*attr_names) # * 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) # * allow_nil Skip validation if attribute is nil (default is false). Notice that for fixnum and float columsn empty strings are converted to nil + # * if - 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) configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save, :only_integer => false, :allow_nil => false } diff --git a/activerecord/test/validations_test.rb b/activerecord/test/validations_test.rb index 4f5f5802ef..13c4a04b62 100755 --- a/activerecord/test/validations_test.rb +++ b/activerecord/test/validations_test.rb @@ -3,6 +3,17 @@ require 'fixtures/reply' 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 fixtures :topics, :developers @@ -688,7 +699,7 @@ def test_validates_numericality_of #assert_in_delta v.to_f, t.approved, 0.0000001 end end - + def test_validates_numericality_of_int_with_string Topic.validates_numericality_of( :approved, :only_integer => true ) ["not a number","42 not a number","0xdeadbeef","0-1","--3","+-3","+3-1",nil].each do |v| @@ -697,7 +708,7 @@ def test_validates_numericality_of_int_with_string assert t.errors.on(:approved) end end - + def test_validates_numericality_of_int Topic.validates_numericality_of( :approved, :only_integer => true, :allow_nil => true ) ["42", "+42", "-42", "042", "0042", "-042", 42, nil,""].each do |v| @@ -707,4 +718,56 @@ def test_validates_numericality_of_int 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 -- GitLab