From 8828b2ca674acfa028a3c1e086a1795d3bb893e1 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Thu, 19 Mar 2009 23:28:59 +0000 Subject: Move all the Active Record validations to Active Model --- .../lib/active_model/validations/acceptance.rb | 21 ++-- .../lib/active_model/validations/associated.rb | 11 +- .../lib/active_model/validations/confirmation.rb | 10 +- .../lib/active_model/validations/exclusion.rb | 16 +-- activemodel/lib/active_model/validations/format.rb | 16 +-- .../lib/active_model/validations/inclusion.rb | 16 +-- activemodel/lib/active_model/validations/length.rb | 80 ++++++------- .../lib/active_model/validations/numericality.rb | 23 ++-- .../lib/active_model/validations/presence.rb | 32 +++--- .../lib/active_model/validations/uniqueness.rb | 125 +++++++++++++++------ 10 files changed, 208 insertions(+), 142 deletions(-) (limited to 'activemodel/lib/active_model/validations') diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 9be7d51ffb..db1fd04bb5 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -8,16 +8,16 @@ module ActiveModel # validates_acceptance_of :eula, :message => "must be abided" # end # - # If the database column does not exist, the :terms_of_service attribute is entirely virtual. This check is - # performed only if :terms_of_service is not +nil+ and by default on save. + # If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is + # performed only if +terms_of_service+ is not +nil+ and by default on save. # # Configuration options: - # * :message - A custom error message (default is: "must be accepted") - # * :on - Specifies when this validation is active (default is :save, other options :create, :update) - # * :allow_nil - Skip validation if attribute is +nil+. (default is +true+) + # * :message - A custom error message (default is: "must be accepted"). + # * :on - Specifies when this validation is active (default is :save, other options :create, :update). + # * :allow_nil - Skip validation if attribute is +nil+ (default is true). # * :accept - Specifies value that is considered accepted. The default value is a string "1", which # makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database - # column, since the attribute is typecasted from "1" to +true+ before validation. + # column, since the attribute is typecast from "1" to +true+ before validation. # * :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. @@ -25,19 +25,22 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => 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 = { :on => :save, :allow_nil => true, :accept => "1" } configuration.update(attr_names.extract_options!) db_cols = begin column_names - rescue ActiveRecord::StatementInvalid + rescue Exception # To ignore both statement and connection errors [] end + names = attr_names.reject { |name| db_cols.include?(name.to_s) } attr_accessor(*names) validates_each(attr_names,configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] + unless value == configuration[:accept] + record.errors.add(attr_name, :accepted, :default => configuration[:message]) + end end end end diff --git a/activemodel/lib/active_model/validations/associated.rb b/activemodel/lib/active_model/validations/associated.rb index b2d78af580..ba4af5d66d 100644 --- a/activemodel/lib/active_model/validations/associated.rb +++ b/activemodel/lib/active_model/validations/associated.rb @@ -18,14 +18,14 @@ module ActiveModel # validates_associated :book # end # - # ...this would specify a circular dependency and cause infinite recursion. + # this would specify a circular dependency and cause infinite recursion. # # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association # is both present and guaranteed to be valid, you also need to use +validates_presence_of+. # # Configuration options: # * :message - A custom error message (default is: "is invalid") - # * :on - Specifies when this validation is active (default is :save, other options :create, :update) + # * :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. @@ -33,12 +33,13 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => 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 = { :on => :save } configuration.update(attr_names.extract_options!) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless - (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all? + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index ba4a18adb7..5f18da58e3 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -21,8 +21,8 @@ module ActiveModel # validates_presence_of :password_confirmation, :if => :password_changed? # # 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) + # * :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. @@ -30,13 +30,15 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => 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 = { :on => :save } configuration.update(attr_names.extract_options!) attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + record.errors.add(attr_name, :confirmation, :default => configuration[:message]) + end end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 1592000fc7..435ba5cab6 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -6,14 +6,14 @@ module ActiveModel # class Person < ActiveRecord::Base # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" - # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed" + # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed" # end # # Configuration options: - # * :in - An enumerable object of items that the value shouldn't be part of - # * :message - Specifies a custom error message (default is: "is reserved") - # * :allow_nil - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * :allow_blank - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * :in - An enumerable object of items that the value shouldn't be part of. + # * :message - Specifies a custom error message (default is: "is reserved"). + # * :allow_nil - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * :allow_blank - If set to true, skips this validation if the attribute is blank (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. @@ -21,7 +21,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => 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 = { :on => :save } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -29,7 +29,9 @@ module ActiveModel raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) + if enum.include?(value) + record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 1320ef646a..c71567b993 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -13,11 +13,11 @@ module ActiveModel # A regular expression must be provided or else an exception will be raised. # # Configuration options: - # * :message - A custom error message (default is: "is invalid") - # * :allow_nil - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * :allow_blank - If set to +true+, skips this validation if the attribute is blank (default is: +false+) - # * :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) + # * :message - A custom error message (default is: "is invalid"). + # * :allow_nil - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * :allow_blank - If set to true, skips this validation if the attribute is blank (default is +false+). + # * :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. @@ -25,13 +25,15 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => 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 = { :on => :save, :with => nil } configuration.update(attr_names.extract_options!) raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] + unless value.to_s =~ configuration[:with] + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index f288810d90..0cb8e44770 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -6,14 +6,14 @@ module ActiveModel # class Person < ActiveRecord::Base # validates_inclusion_of :gender, :in => %w( m f ) # validates_inclusion_of :age, :in => 0..99 - # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list" + # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list" # end # # Configuration options: - # * :in - An enumerable object of available items - # * :message - Specifies a custom 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+) - # * :allow_blank - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * :in - An enumerable object of available items. + # * :message - Specifies a custom error message (default is: "is not included in the list"). + # * :allow_nil - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * :allow_blank - If set to true, skips this validation if the attribute is blank (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. @@ -21,7 +21,7 @@ module ActiveModel # not occur (e.g. :unless => :skip_validation, or :unless => 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 = { :on => :save } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -29,7 +29,9 @@ module ActiveModel raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) + unless enum.include?(value) + record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) + end end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 673ad33974..f6bb1f6d70 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -2,44 +2,46 @@ module ActiveModel module Validations module ClassMethods ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze - + # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # # class Person < ActiveRecord::Base - # validates_length_of :first_name, :maximum => 30 - # validates_length_of :last_name, :maximum => 30, :message => "less than %d if you don't mind" + # validates_length_of :first_name, :maximum=>30 + # validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind" # validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :phone, :in => 7..32, :allow_blank => true # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" - # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character" - # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me." + # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character" + # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me." + # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) } # end # # Configuration options: - # * :minimum - The minimum size of the attribute - # * :maximum - The maximum size of the attribute - # * :is - The exact size of the attribute - # * :within - A range specifying the minimum and maximum size of the attribute - # * :in - A synonym (or alias) for :within + # * :minimum - The minimum size of the attribute. + # * :maximum - The maximum size of the attribute. + # * :is - The exact size of the attribute. + # * :within - A range specifying the minimum and maximum size of the attribute. + # * :in - A synonym(or alias) for :within. # * :allow_nil - Attribute may be +nil+; skip validation. # * :allow_blank - Attribute may be blank; skip validation. - # * :too_long - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)") - # * :too_short - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)") - # * :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) + # * :too_long - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)"). + # * :too_short - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)"). + # * :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 {{count}} 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. # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. + # * :tokenizer - Specifies how to split up the attribute string. (e.g. :tokenizer => lambda {|str| str.scan(/\w+/)} to + # count words as in above example.) + # Defaults to lambda{ |value| value.split(//) } which counts individual characters. def validates_length_of(*attrs) # Merge given options with defaults. options = { - :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] + :tokenizer => lambda {|value| value.split(//)} }.merge(DEFAULT_VALIDATION_OPTIONS) options.update(attrs.extract_options!.symbolize_keys) @@ -57,35 +59,33 @@ module ActiveModel # Get range option and value. option = range_options.first option_value = options[range_options.first] + key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option] + custom_message = options[:message] || options[key] case option - when :within, :in - raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - - too_short = options[:too_short] % option_value.begin - too_long = options[:too_long] % option_value.end + when :within, :in + raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - validates_each(attrs, options) do |record, attr, value| - value = value.split(//) if value.kind_of?(String) - if value.nil? or value.size < option_value.begin - record.errors.add(attr, too_short) - elsif value.size > option_value.end - record.errors.add(attr, too_long) - end + validates_each(attrs, options) do |record, attr, value| + value = options[:tokenizer].call(value) if value.kind_of?(String) + if value.nil? or value.size < option_value.begin + record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin) + elsif value.size > option_value.end + record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end) end - when :is, :minimum, :maximum - raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 - - # Declare different validations per option. - validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } + end + when :is, :minimum, :maximum + raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 - message = (options[:message] || options[message_options[option]]) % option_value + # Declare different validations per option. + validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - validates_each(attrs, options) do |record, attr, value| - value = value.split(//) if value.kind_of?(String) - record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + validates_each(attrs, options) do |record, attr, value| + value = options[:tokenizer].call(value) if value.kind_of?(String) + unless !value.nil? and value.size.method(validity_checks[option])[option_value] + record.errors.add(attr, key, :default => custom_message, :count => option_value) end + end end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 92ca5f4082..d8e3470304 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -5,10 +5,9 @@ module ActiveModel :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', :odd => 'odd?', :even => 'even?' }.freeze - # 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 - # /\A[\+\-]?\d+\Z/ (if integer is true). + # a float with Kernel.Float (if only_integer is false) or applying it to the regular expression + # /\A[\+\-]?\d+\Z/ (if only_integer is set to true). # # class Person < ActiveRecord::Base # validates_numericality_of :value, :on => :create @@ -50,15 +49,15 @@ module ActiveModel if configuration[:only_integer] unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) next end raw_value = raw_value.to_i else - begin - raw_value = Kernel.Float(raw_value.to_s) + begin + raw_value = Kernel.Float(raw_value) rescue ArgumentError, TypeError - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) next end end @@ -66,11 +65,13 @@ module ActiveModel numericality_options.each do |option| case option when :odd, :even - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) + end else - message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option] - message = message % configuration[option] if configuration[option] - record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] + unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] + record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) + end end end end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 62e466901b..4fd60621cd 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -7,28 +7,26 @@ module ActiveModel # validates_presence_of :first_name # end # - # The +first_name+ attribute must be in the object and it cannot be blank. + # The first_name attribute must be in the object and it cannot be blank. # - # If you want to validate the presence of a boolean field (where the real values are +true+ and +false+), - # you will want to use + # If you want to validate the presence of a boolean field (where the real values are true and false), + # you will want to use validates_inclusion_of :field_name, :in => [true, false]. # - # validates_inclusion_of :field_name, :in => [true, false] - # - # This is due to the way Object#blank? handles boolean values: - # - # false.blank? # => true + # This is due to the way Object#blank? handles boolean values: false.blank? # => true. # # Configuration options: - # * :message - A custom error message (default is: "can't be blank") - # * :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. - # * :unless - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The - # method, proc or string should return or evaluate to a true or false value. + # * message - A custom error message (default is: "can't be blank"). + # * 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. + # * unless - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. :unless => :skip_validation, or :unless => 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[:blank], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) # can't use validates_each here, because it cannot cope with nonexistent attributes, diff --git a/activemodel/lib/active_model/validations/uniqueness.rb b/activemodel/lib/active_model/validations/uniqueness.rb index 593d2cfd54..37beac863f 100644 --- a/activemodel/lib/active_model/validations/uniqueness.rb +++ b/activemodel/lib/active_model/validations/uniqueness.rb @@ -18,24 +18,84 @@ module ActiveModel # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. # - # Because this check is performed outside the database there is still a chance that duplicate values - # will be inserted in two parallel transactions. To guarantee against this you should create a - # unique index on the field. See +add_index+ for more information. - # # Configuration options: - # * :message - Specifies a custom error message (default is: "has already been taken") + # * :message - Specifies a custom error message (default is: "has already been taken"). # * :scope - One or more columns by which to limit the scope of the uniqueness constraint. - # * :case_sensitive - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * :allow_nil - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * :allow_blank - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * :case_sensitive - Looks for an exact match. Ignored by non-text columns (+true+ by default). + # * :allow_nil - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * :allow_blank - If set to true, skips this validation if the attribute is blank (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. # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. + # + # === Concurrency and integrity + # + # Using this validation method in conjunction with ActiveRecord::Base#save + # does not guarantee the absence of duplicate record insertions, because + # uniqueness checks on the application level are inherently prone to race + # conditions. For example, suppose that two users try to post a Comment at + # the same time, and a Comment's title must be unique. At the database-level, + # the actions performed by these users could be interleaved in the following manner: + # + # User 1 | User 2 + # ------------------------------------+-------------------------------------- + # # User 1 checks whether there's | + # # already a comment with the title | + # # 'My Post'. This is not the case. | + # SELECT * FROM comments | + # WHERE title = 'My Post' | + # | + # | # User 2 does the same thing and also + # | # infers that his title is unique. + # | SELECT * FROM comments + # | WHERE title = 'My Post' + # | + # # User 1 inserts his comment. | + # INSERT INTO comments | + # (title, content) VALUES | + # ('My Post', 'hi!') | + # | + # | # User 2 does the same thing. + # | INSERT INTO comments + # | (title, content) VALUES + # | ('My Post', 'hello!') + # | + # | # ^^^^^^ + # | # Boom! We now have a duplicate + # | # title! + # + # This could even happen if you use transactions with the 'serializable' + # isolation level. There are several ways to get around this problem: + # - By locking the database table before validating, and unlocking it after + # saving. However, table locking is very expensive, and thus not + # recommended. + # - By locking a lock file before validating, and unlocking it after saving. + # This does not work if you've scaled your Rails application across + # multiple web servers (because they cannot share lock files, or cannot + # do that efficiently), and thus not recommended. + # - Creating a unique index on the field, by using + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the + # rare case that a race condition occurs, the database will guarantee + # the field's uniqueness. + # + # When the database catches such a duplicate insertion, + # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # exception. You can either choose to let this error propagate (which + # will result in the default Rails exception page being shown), or you + # can catch it and restart the transaction (e.g. by telling the user + # that the title already exists, and asking him to re-enter the title). + # This technique is also known as optimistic concurrency control: + # http://en.wikipedia.org/wiki/Optimistic_concurrency_control + # + # Active Record currently provides no way to distinguish unique + # index constraint errors from other types of database errors, so you + # will have to parse the (database-specific) exception message to detect + # such a case. def validates_uniqueness_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] } + configuration = { :case_sensitive => true } configuration.update(attr_names.extract_options!) validates_each(attr_names,configuration) do |record, attr_name, value| @@ -53,20 +113,31 @@ module ActiveModel # class (which has a database table to query from). finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?) - condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" + column = finder_class.columns_hash[attr_name.to_s] + + if value.nil? + comparison_operator = "IS ?" + elsif column.text? + comparison_operator = "#{connection.case_sensitive_equality_operator} ?" + value = column.limit ? value.to_s[0, column.limit] : value.to_s + else + comparison_operator = "= ?" + end + + sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" + + if value.nil? || (configuration[:case_sensitive] || !column.text?) + condition_sql = "#{sql_attribute} #{comparison_operator}" condition_params = [value] else - # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't. - # Hence, this is needed only for sqlite. - condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" - condition_params = [value.downcase] + condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" + condition_params = [value.mb_chars.downcase] end if scope = configuration[:scope] Array(scope).map do |scope_item| scope_value = record.send(scope_item) - condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}" + condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) condition_params << scope_value end end @@ -76,26 +147,10 @@ module ActiveModel condition_params << record.send(:id) end - results = finder_class.with_exclusive_scope do - connection.select_all( - construct_finder_sql( - :select => attr_name, - :from => finder_class.quoted_table_name, - :conditions => [condition_sql, *condition_params] - ) - ) - end - - unless results.length.zero? - found = true - - # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate - # column in ruby when case sensitive option - if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? - found = results.any? { |a| a[attr_name.to_s] == value } + finder_class.with_exclusive_scope do + if finder_class.exists?([condition_sql, *condition_params]) + record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value) end - - record.errors.add(attr_name, configuration[:message]) if found end end end -- cgit v1.2.3