diff options
69 files changed, 3729 insertions, 3313 deletions
diff --git a/.gitignore b/.gitignore index d86af64569..5357fbe481 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ activesupport/doc activeresource/pkg activerecord/pkg actionpack/pkg +activemodel/test/fixtures/fixture_database.sqlite3 actionmailer/pkg activesupport/pkg railties/pkg diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 4b60f8d682..283380db96 100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -7,9 +7,8 @@ task :default => :test Rake::TestTask.new do |t| t.libs << "test" - t.pattern = 'test/**/*_test.rb' + t.test_files = Dir.glob("test/cases/**/*_test.rb").sort t.verbose = true - t.warning = true end # Generate the RDoc documentation diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 4ed7b0889d..7f943b60ce 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,45 @@ -require 'active_model/observing' -# disabled until they're tested -# require 'active_model/callbacks' -# require 'active_model/validations' -require 'active_model/base'
\ No newline at end of file +#-- +# Copyright (c) 2004-2009 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +begin + require 'active_support' +rescue LoadError + activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" + if File.directory?(activesupport_path) + $:.unshift activesupport_path + require 'active_support' + end +end + +module ActiveModel + autoload :Base, 'active_model/base' + autoload :Observing, 'active_model/observing' + autoload :Validations, 'active_model/validations' + autoload :Errors, 'active_model/errors' + autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods' + autoload :TestCase, 'active_model/test_case' + autoload :StateMachine, 'active_model/state_machine' + autoload :ValidationsRepairHelper, 'active_model/validations_repair_helper' +end + +I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb deleted file mode 100644 index c94f76109f..0000000000 --- a/activemodel/lib/active_model/callbacks.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'active_model/core' - -module ActiveModel - module Callbacks - - end -end
\ No newline at end of file diff --git a/activemodel/lib/active_model/core.rb b/activemodel/lib/active_model/core.rb deleted file mode 100644 index 47b968e121..0000000000 --- a/activemodel/lib/active_model/core.rb +++ /dev/null @@ -1,7 +0,0 @@ -# This file is required by each major ActiveModel component for the core requirements. This allows you to -# load individual pieces of ActiveModel as needed. -$LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..', '..', 'activesupport', 'lib') - -# premature optimization? -# So far, we only need the string inflections and not the rest of ActiveSupport. -require 'active_support/inflector'
\ No newline at end of file diff --git a/activemodel/lib/active_model/deprecated_error_methods.rb b/activemodel/lib/active_model/deprecated_error_methods.rb index e0cbd9ba29..433de8931a 100644 --- a/activemodel/lib/active_model/deprecated_error_methods.rb +++ b/activemodel/lib/active_model/deprecated_error_methods.rb @@ -1,18 +1,18 @@ module ActiveModel module DeprecatedErrorMethods def on(attribute) - ActiveSupport::Deprecation.warn "Errors#on have been deprecated, use Errors#[] instead" - self[attribute] + message = "Errors#on have been deprecated, use Errors#[] instead.\n" + message << "Also note that the behaviour of Errors#[] has changed. Errors#[] now always returns an Array. An empty Array is " + message << "returned when there are no errors on the specified attribute." + ActiveSupport::Deprecation.warn(message) + + errors = self[attribute] + errors.size < 2 ? errors.first : errors end def on_base ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead" - on(:base) - end - - def add(attribute, msg = Errors.default_error_messages[:invalid]) - ActiveSupport::Deprecation.warn "Errors#add(attribute, msg) has been deprecated, use Errors#[attribute] << msg instead" - self[attribute] << msg + ActiveSupport::Deprecation.silence { on(:base) } end def add_to_base(msg) @@ -25,11 +25,6 @@ module ActiveModel self[attribute].any? end - def full_messages - ActiveSupport::Deprecation.warn "Errors#full_messages has been deprecated, use Errors#to_a instead" - to_a - end - def each_full ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead" to_a.each { |error| yield error } diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index bcf0810290..c2bde29895 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,40 +1,18 @@ module ActiveModel class Errors < Hash include DeprecatedErrorMethods - - @@default_error_messages = { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is %d characters)", - :too_short => "is too short (minimum 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", - :greater_than => "must be greater than %d", - :greater_than_or_equal_to => "must be greater than or equal to %d", - :equal_to => "must be equal to %d", - :less_than => "must be less than %d", - :less_than_or_equal_to => "must be less than or equal to %d", - :odd => "must be odd", - :even => "must be even" - } - - ## - # :singleton-method: - # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. - cattr_accessor :default_error_messages + + def initialize(base) + @base = base + super() + end alias_method :get, :[] alias_method :set, :[]= def [](attribute) if errors = get(attribute.to_sym) - errors.size == 1 ? errors.first : errors + errors else set(attribute.to_sym, []) end @@ -55,17 +33,11 @@ module ActiveModel end def to_a - inject([]) do |errors_with_attributes, (attribute, errors)| - if error.blank? - errors_with_attributes - else - if attr == :base - errors_with_attributes << error - else - errors_with_attributes << (attribute.to_s.humanize + " " + error) - end - end - end + full_messages + end + + def count + to_a.size end def to_xml(options={}) @@ -78,5 +50,108 @@ module ActiveModel to_a.each { |error| e.error(error) } end end + + # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> + # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one + # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. + # If no +messsage+ is supplied, :invalid is assumed. + # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). + def add(attribute, message = nil, options = {}) + message ||= :invalid + message = generate_message(attribute, message, options) if message.is_a?(Symbol) + self[attribute] << message + end + + # Will add an error message to each of the attributes in +attributes+ that is empty. + def add_on_empty(attributes, custom_message = nil) + [attributes].flatten.each do |attribute| + value = @base.get_attribute_value(attribute) + is_empty = value.respond_to?(:empty?) ? value.empty? : false + add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty + end + end + + # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). + def add_on_blank(attributes, custom_message = nil) + [attributes].flatten.each do |attribute| + value = @base.get_attribute_value(attribute) + add(attribute, :blank, :default => custom_message) if value.blank? + end + end + + # Returns all the full error messages in an array. + # + # class Company + # validates_presence_of :name, :address, :email + # validates_length_of :name, :in => 5..30 + # end + # + # company = Company.create(:address => '123 First St.') + # company.errors.full_messages # => + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + def full_messages(options = {}) + full_messages = [] + + each do |attribute, messages| + next if messages.empty? + + if attribute == :base + messages.each {|m| full_messages << m } + else + attr_name = attribute.to_s.humanize + prefix = attr_name + I18n.t('activemodel.errors.format.separator', :default => ' ') + messages.each do |m| + full_messages << "#{prefix}#{m}" + end + end + end + + full_messages + end + + # Translates an error message in it's default scope (<tt>activemodel.errrors.messages</tt>). + # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, + # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the + # default message (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name, + # translated attribute name and the value are available for interpolation. + # + # When using inheritence in your models, it will check all the inherited models too, but only if the model itself + # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt> + # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations: + # + # <ol> + # <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li> + # <li><tt>activemodel.errors.models.admin.blank</tt></li> + # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li> + # <li><tt>activemodel.errors.models.user.blank</tt></li> + # <li><tt>activemodel.errors.messages.blank</tt></li> + # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li> + # </ol> + def generate_message(attribute, message = :invalid, options = {}) + message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) + + klass_ancestors = [@base.class] + klass_ancestors += @base.class.ancestors.reject {|x| x.is_a?(Module)} + + defaults = klass_ancestors.map do |klass| + [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", + :"models.#{klass.name.underscore}.#{message}" ] + end + + defaults << options.delete(:default) + defaults = defaults.compact.flatten << :"messages.#{message}" + + key = defaults.shift + value = @base.get_attribute_value(attribute) + + options = { :default => defaults, + :model => @base.class.name.humanize, + :attribute => attribute.to_s.humanize, + :value => value, + :scope => [:activemodel, :errors] + }.merge(options) + + I18n.translate(key, options) + end end end
\ No newline at end of file diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml new file mode 100644 index 0000000000..0c2cf9ea33 --- /dev/null +++ b/activemodel/lib/active_model/locale/en.yml @@ -0,0 +1,24 @@ +en: + activemodel: + errors: + # The values :model, :attribute and :value are always available for interpolation + # The value :count is available when applicable. Can be used for pluralization. + messages: + inclusion: "is not included in the list" + exclusion: "is reserved" + invalid: "is invalid" + confirmation: "doesn't match confirmation" + accepted: "must be accepted" + empty: "can't be empty" + blank: "can't be blank" + too_long: "is too long (maximum is {{count}} characters)" + too_short: "is too short (minimum is {{count}} characters)" + wrong_length: "is the wrong length (should be {{count}} characters)" + not_a_number: "is not a number" + greater_than: "must be greater than {{count}}" + greater_than_or_equal_to: "must be greater than or equal to {{count}}" + equal_to: "must be equal to {{count}}" + less_than: "must be less than {{count}}" + less_than_or_equal_to: "must be less than or equal to {{count}}" + odd: "must be odd" + even: "must be even" diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 9e99d7472c..d3c6d8e482 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -1,6 +1,5 @@ require 'observer' require 'singleton' -require 'active_model/core' module ActiveModel module Observing diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb new file mode 100644 index 0000000000..4cb5c9cbc0 --- /dev/null +++ b/activemodel/lib/active_model/test_case.rb @@ -0,0 +1,18 @@ +require "active_support/test_case" + +module ActiveModel #:nodoc: + class TestCase < ActiveSupport::TestCase #:nodoc: + def with_kcode(kcode) + if RUBY_VERSION < '1.9' + orig_kcode, $KCODE = $KCODE, kcode + begin + yield + ensure + $KCODE = orig_kcode + end + else + yield + end + end + end +end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 460d2d82e5..64343f7a5c 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,16 +1,12 @@ -require 'active_model/core' - module ActiveModel module Validations def self.included(base) # :nodoc: base.extend(ClassMethods) base.__send__(:include, ActiveSupport::Callbacks) - base.define_callbacks :validate, :validate_on_create, :validate_on_update + base.define_callbacks :validate end module ClassMethods - DEFAULT_VALIDATION_OPTIONS = { :on => :save, :allow_nil => false, :allow_blank => false, :message => nil }.freeze - # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldly and # you're looking for more descriptive declaration of your validations. @@ -38,7 +34,7 @@ module ActiveModel # end # # This usage applies to +validate_on_create+ and +validate_on_update as well+. - # + # Validates each attribute against a block. # # class Person < ActiveRecord::Base @@ -48,7 +44,7 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should @@ -62,9 +58,9 @@ module ActiveModel attrs = attrs.flatten # Declare the validation. - send(validation_method(options[:on] || :save), options) do |record| + send(validation_method(options[:on]), options) do |record| attrs.each do |attr| - value = record.send(attr) + value = record.get_attribute_value(attr) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) yield record, attr, value end @@ -72,50 +68,31 @@ module ActiveModel end private - def validation_method(on) - case on - when :save then :validate - when :create then :validate_on_create - when :update then :validate_on_update - end - end + + def validation_method(on) + :validate + end end # Returns the Errors object that holds all information about attribute error messages. def errors - @errors ||= Errors.new + @errors ||= Errors.new(self) end # Runs all the specified validations and returns true if no errors were added otherwise false. def valid? errors.clear - run_callbacks(:validate) - - if responds_to?(:validate) - ActiveSupport::Deprecations.warn "Base#validate has been deprecated, please use Base.validate :method instead" - validate - end - - if new_record? - run_callbacks(:validate_on_create) - - if responds_to?(:validate_on_create) - ActiveSupport::Deprecations.warn( - "Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead") - validate_on_create - end - else - run_callbacks(:validate_on_update) + errors.empty? + end - if responds_to?(:validate_on_update) - ActiveSupport::Deprecations.warn( - "Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead") - validate_on_update - end - end + # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise. + def invalid? + !valid? + end - errors.empty? + def get_attribute_value(attribute) + respond_to?(attribute.to_sym) ? send(attribute.to_sym) : instance_variable_get(:"@#{attribute}") end end end @@ -123,4 +100,4 @@ end Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| filename = File.basename(path) require "active_model/validations/#{filename}" -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 9be7d51ffb..0c9ef51726 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 <tt>:terms_of_service</tt> attribute is entirely virtual. This check is - # performed only if <tt>:terms_of_service</tt> 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: - # * <tt>:message</tt> - A custom error message (default is: "must be accepted") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. (default is +true+) + # * <tt>:message</tt> - A custom error message (default is: "must be accepted"). + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true). # * <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. 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. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). 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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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 = { :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/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index ba4a18adb7..b9823172f7 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: - # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation"). + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -30,13 +30,14 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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.extract_options!) + configuration = 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..0aa9848ee1 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: - # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of - # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved") - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. + # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -21,15 +21,16 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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.extract_options!) + configuration = attr_names.extract_options! enum = configuration[:in] || configuration[:within] 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..8efce8ba2b 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: - # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) - # * <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 <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:message</tt> - A custom error message (default is: "is invalid"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <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 <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). 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. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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 = { :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..a4bc8fe035 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: - # * <tt>:in</tt> - An enumerable object of available items - # * <tt>:message</tt> - Specifies a custom 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_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) + # * <tt>:in</tt> - An enumerable object of available items. + # * <tt>:message</tt> - Specifies a custom 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 +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -21,15 +21,16 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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.extract_options!) + configuration = attr_names.extract_options! enum = configuration[:in] || configuration[:within] 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..bb9a269a02 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -2,90 +2,88 @@ 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: - # * <tt>:minimum</tt> - The minimum size of the attribute - # * <tt>:maximum</tt> - The maximum size of the attribute - # * <tt>:is</tt> - The exact size of the attribute - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute - # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt> + # * <tt>:minimum</tt> - The minimum size of the attribute. + # * <tt>:maximum</tt> - The maximum size of the attribute. + # * <tt>:is</tt> - The exact size of the attribute. + # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. + # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)") - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)") - # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> 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 <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>:too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)"). + # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)"). + # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)"). + # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. + # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to + # count words as in above example.) + # Defaults to <tt>lambda{ |value| value.split(//) }</tt> 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] - }.merge(DEFAULT_VALIDATION_OPTIONS) + options = { :tokenizer => lambda {|value| value.split(//)} } options.update(attrs.extract_options!.symbolize_keys) # Ensure that one and only one range option is specified. range_options = ALL_RANGE_OPTIONS & options.keys case range_options.size - when 0 - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' - when 1 - # Valid number of options; do nothing. - else - raise ArgumentError, 'Too many range options specified. Choose only one.' + when 0 + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + when 1 + # Valid number of options; do nothing. + else + raise ArgumentError, 'Too many range options specified. Choose only one.' end # 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..79fca2f1ea 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 <tt>integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>integer</tt> is true). + # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression + # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). # # class Person < ActiveRecord::Base # validates_numericality_of :value, :on => :create @@ -33,10 +32,9 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_numericality_of(*attr_names) - configuration = { :on => :save, :only_integer => false, :allow_nil => false } + configuration = { :only_integer => false, :allow_nil => false } configuration.update(attr_names.extract_options!) - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys (numericality_options - [ :odd, :even ]).each do |option| @@ -44,33 +42,41 @@ module ActiveModel end validates_each(attr_names,configuration) do |record, attr_name, value| - raw_value = record.send("#{attr_name}_before_type_cast") || value + before_type_cast = "#{attr_name}_before_type_cast" + + if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send("#{attr_name}_before_type_cast") || value + else + raw_value = value + end next if configuration[:allow_nil] and raw_value.nil? 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 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])[] - 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]] + when :odd, :even + unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) + end + else + 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..518bc8a952 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -7,29 +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 <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. # - # 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: <tt>false.blank? # => true</tt>. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "can't be blank") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # * <tt>message</tt> - A custom error message (default is: "can't be blank"). + # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, + # <tt>:update</tt>). + # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). + # The method, proc or string should return or evaluate to a true or false value. + # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). + # 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.update(attr_names.extract_options!) + configuration = attr_names.extract_options! # can't use validates_each here, because it cannot cope with nonexistent attributes, # while errors.add_on_empty can diff --git a/activemodel/lib/active_model/validations/uniqueness.rb b/activemodel/lib/active_model/validations/uniqueness.rb deleted file mode 100644 index 593d2cfd54..0000000000 --- a/activemodel/lib/active_model/validations/uniqueness.rb +++ /dev/null @@ -1,104 +0,0 @@ -module ActiveModel - module Validations - module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user - # can be named "davidhh". - # - # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id - # end - # - # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, - # making sure that a teacher can only be on the schedule once per semester for a particular class. - # - # class TeacherSchedule < ActiveRecord::Base - # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] - # end - # - # 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: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken") - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the attribute is +nil+ (default is: +false+) - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the attribute is blank (default is: +false+) - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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.extract_options!) - - validates_each(attr_names,configuration) do |record, attr_name, value| - # The check for an existing value should be run from a class that - # isn't abstract. This means working down from the current class - # (self), to the first non-abstract class. Since classes don't know - # their subclasses, we have to build the hierarchy between self and - # the record's class. - class_hierarchy = [record.class] - while class_hierarchy.first != self - class_hierarchy.insert(0, class_hierarchy.first.superclass) - end - - # Now we can work our way down the tree to the first non-abstract - # 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)}" - 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] - 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_params << scope_value - end - end - - unless record.new_record? - condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" - 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 } - end - - record.errors.add(attr_name, configuration[:message]) if found - end - end - end - end - end -end diff --git a/activemodel/lib/active_model/validations_repair_helper.rb b/activemodel/lib/active_model/validations_repair_helper.rb new file mode 100644 index 0000000000..99e3b2f631 --- /dev/null +++ b/activemodel/lib/active_model/validations_repair_helper.rb @@ -0,0 +1,48 @@ +module ActiveModel + module ValidationsRepairHelper + def self.included(base) + base.class_eval do + extend ClassMethods + end + end + + module Toolbox + def self.record_validations(*model_classes) + model_classes.inject({}) do |repair, klass| + repair[klass] ||= {} + [:validate, :validate_on_create, :validate_on_update].each do |callback| + the_callback = klass.instance_variable_get("@#{callback.to_s}_callbacks") + repair[klass][callback] = (the_callback.nil? ? nil : the_callback.dup) + end + repair + end + end + + def self.reset_validations(recorded) + recorded.each do |klass, repairs| + [:validate, :validate_on_create, :validate_on_update].each do |callback| + klass.instance_variable_set("@#{callback.to_s}_callbacks", repairs[callback]) + end + end + end + end + + module ClassMethods + def repair_validations(*model_classes) + setup do + @validation_repairs = Toolbox.record_validations(*model_classes) + end + teardown do + Toolbox.reset_validations(@validation_repairs) + end + end + end + + def repair_validations(*model_classes, &block) + validation_repairs = Toolbox.record_validations(*model_classes) + return block.call + ensure + Toolbox.reset_validations(validation_repairs) + end + end +end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb new file mode 100644 index 0000000000..f023109a60 --- /dev/null +++ b/activemodel/test/cases/helper.rb @@ -0,0 +1,20 @@ +$:.unshift(File.dirname(__FILE__) + '/../../lib') +$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib') + +require 'config' + +require 'active_model' + +# Show backtraces for deprecated behavior for quicker cleanup. +ActiveSupport::Deprecation.debug = true + +require 'rubygems' +require 'test/unit' +gem 'mocha', '>= 0.9.5' +require 'mocha' + +begin + require 'ruby-debug' + Debugger.start +rescue LoadError +end diff --git a/activemodel/test/observing_test.rb b/activemodel/test/cases/observing_test.rb index dc41c9f881..421ac4b4f8 100644 --- a/activemodel/test/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class ObservedModel < ActiveModel::Base class Observer diff --git a/activemodel/test/state_machine/event_test.rb b/activemodel/test/cases/state_machine/event_test.rb index 64dc8c4875..2a0ef53a3f 100644 --- a/activemodel/test/state_machine/event_test.rb +++ b/activemodel/test/cases/state_machine/event_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class EventTest < ActiveModel::TestCase def setup diff --git a/activemodel/test/state_machine/machine_test.rb b/activemodel/test/cases/state_machine/machine_test.rb index d23c223160..2aa954d80c 100644 --- a/activemodel/test/state_machine/machine_test.rb +++ b/activemodel/test/cases/state_machine/machine_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class MachineTestSubject include ActiveModel::StateMachine diff --git a/activemodel/test/state_machine/state_test.rb b/activemodel/test/cases/state_machine/state_test.rb index fbf9ce7b0a..527bfd4c04 100644 --- a/activemodel/test/state_machine/state_test.rb +++ b/activemodel/test/cases/state_machine/state_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class StateTestSubject include ActiveModel::StateMachine diff --git a/activemodel/test/state_machine/state_transition_test.rb b/activemodel/test/cases/state_machine/state_transition_test.rb index b59ff5a6a7..17f9d88be7 100644 --- a/activemodel/test/state_machine/state_transition_test.rb +++ b/activemodel/test/cases/state_machine/state_transition_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class StateTransitionTest < ActiveModel::TestCase test 'should set from, to, and opts attr readers' do diff --git a/activemodel/test/state_machine_test.rb b/activemodel/test/cases/state_machine_test.rb index 312d8728ba..f66299741e 100644 --- a/activemodel/test/state_machine_test.rb +++ b/activemodel/test/cases/state_machine_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require 'cases/helper' class StateMachineSubject include ActiveModel::StateMachine diff --git a/activemodel/test/cases/tests_database.rb b/activemodel/test/cases/tests_database.rb new file mode 100644 index 0000000000..0f4475fa2d --- /dev/null +++ b/activemodel/test/cases/tests_database.rb @@ -0,0 +1,41 @@ +require 'logger' + +$:.unshift(File.dirname(__FILE__) + '/../../../activerecord/lib') +require 'active_record' +require 'active_record/fixtures' + +module ActiveModel + module TestsDatabase + mattr_accessor :connected + + def self.included(base) + unless self.connected + setup_connection + setup_schema + end + + base.send :include, ActiveRecord::TestFixtures + end + + def self.setup_schema + original, $stdout = $stdout, StringIO.new + load(SCHEMA_FILE) + ensure + $stdout = original + self.connected = true + end + + def self.setup_connection + defaults = { :database => ':memory:' } + begin + adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' + options = defaults.merge :adapter => adapter, :timeout => 500 + ActiveRecord::Base.establish_connection(options) + rescue Exception + $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.' + options = defaults.merge :adapter => 'sqlite' + ActiveRecord::Base.establish_connection(options) + end + end + end +end diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb new file mode 100644 index 0000000000..88e5fdb358 --- /dev/null +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -0,0 +1,80 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/reply' +require 'models/developer' +require 'models/person' + +class AcceptanceValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_terms_of_service_agreement_no_acceptance + Topic.validates_acceptance_of(:terms_of_service, :on => :create) + + t = Topic.create("title" => "We should not be confirmed") + assert t.save + end + + def test_terms_of_service_agreement + Topic.validates_acceptance_of(:terms_of_service, :on => :create) + + t = Topic.create("title" => "We should be confirmed","terms_of_service" => "") + assert !t.save + assert_equal ["must be accepted"], t.errors[:terms_of_service] + + t.terms_of_service = "1" + assert t.save + end + + def test_eula + Topic.validates_acceptance_of(:eula, :message => "must be abided", :on => :create) + + t = Topic.create("title" => "We should be confirmed","eula" => "") + assert !t.save + assert_equal ["must be abided"], t.errors[:eula] + + t.eula = "1" + assert t.save + end + + def test_terms_of_service_agreement_with_accept_value + Topic.validates_acceptance_of(:terms_of_service, :on => :create, :accept => "I agree.") + + t = Topic.create("title" => "We should be confirmed", "terms_of_service" => "") + assert !t.save + assert_equal ["must be accepted"], t.errors[:terms_of_service] + + t.terms_of_service = "I agree." + assert t.save + end + + def test_validates_acceptance_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.salary = "0" + assert !d.valid? + assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last + end + end + + def test_validates_acceptance_of_for_ruby_class + repair_validations(Person) do + Person.validates_acceptance_of :karma + + p = Person.new + p.karma = "" + + assert p.invalid? + assert_equal ["must be accepted"], p.errors[:karma] + + p.karma = "1" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb new file mode 100644 index 0000000000..4c716d5d48 --- /dev/null +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -0,0 +1,140 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' + +class ConditionalValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_if_validation_using_method_true + # When the method returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_unless_validation_using_method_true + # When the method returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert !t.errors[:title].any? + end + + def test_if_validation_using_method_false + # When the method returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true_but_its_not ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_unless_validation_using_method_false + # When the method returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true_but_its_not ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_if_validation_using_string_true + # When the evaluated string returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "a = 1; a == 1" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_unless_validation_using_string_true + # When the evaluated string returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "a = 1; a == 1" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_if_validation_using_string_false + # When the evaluated string returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "false") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_unless_validation_using_string_false + # When the evaluated string returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "false") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_if_validation_using_block_true + # When the block returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :if => Proc.new { |r| r.content.size > 4 } ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_unless_validation_using_block_true + # When the block returns true + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :unless => Proc.new { |r| r.content.size > 4 } ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_if_validation_using_block_false + # When the block returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :if => Proc.new { |r| r.title != "uhohuhoh"} ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert t.valid? + assert t.errors[:title].empty? + end + + def test_unless_validation_using_block_false + # When the block returns false + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", + :unless => Proc.new { |r| r.title != "uhohuhoh"} ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + # previous implementation of validates_presence_of eval'd the + # string with the wrong binding, this regression test is to + # ensure that it works correctly + def test_validation_with_if_as_string + Topic.validates_presence_of(:title) + Topic.validates_presence_of(:author_name, :if => "title.to_s.match('important')") + + t = Topic.new + assert t.invalid?, "A topic without a title should not be valid" + assert t.errors[:author_name].empty?, "A topic without an 'important' title should not require an author" + + t.title = "Just a title" + assert t.valid?, "A topic with a basic title should be valid" + + t.title = "A very important title" + assert !t.valid?, "A topic with an important title, but without an author, should not be valid" + assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author" + + t.author_name = "Hubert J. Farnsworth" + assert t.valid?, "A topic with an important title and author should be valid" + end +end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb new file mode 100644 index 0000000000..1d6f2a6ec5 --- /dev/null +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -0,0 +1,68 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class ConfirmationValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_no_title_confirmation + Topic.validates_confirmation_of(:title) + + t = Topic.new(:author_name => "Plutarch") + assert t.valid? + + t.title_confirmation = "Parallel Lives" + assert !t.valid? + + t.title_confirmation = nil + t.title = "Parallel Lives" + assert t.valid? + + t.title_confirmation = "Parallel Lives" + assert t.valid? + end + + def test_title_confirmation + Topic.validates_confirmation_of(:title) + + t = Topic.create("title" => "We should be confirmed","title_confirmation" => "") + assert !t.save + + t.title_confirmation = "We should be confirmed" + assert t.save + end + + def test_validates_confirmation_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes" + d = Developer.new + d.name = "John" + d.name_confirmation = "Johnny" + assert !d.valid? + assert_equal ["confirm 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_confirmation_of_for_ruby_class + repair_validations(Person) do + Person.validates_confirmation_of :karma + + p = Person.new + p.karma_confirmation = "None" + assert p.invalid? + + assert_equal ["doesn't match confirmation"], p.errors[:karma] + + p.karma = "None" + assert p.valid? + end + end + +end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb new file mode 100644 index 0000000000..584f009e84 --- /dev/null +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -0,0 +1,46 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/person' + +class ExclusionValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validates_exclusion_of + Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) ) + + assert Topic.create("title" => "something", "content" => "abc").valid? + assert !Topic.create("title" => "monkey", "content" => "abc").valid? + end + + def test_validates_exclusion_of_with_formatted_message + Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option {{value}} is restricted" ) + + assert Topic.create("title" => "something", "content" => "abc") + + t = Topic.create("title" => "monkey") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["option monkey is restricted"], t.errors[:title] + end + + def test_validates_exclusion_of_for_ruby_class + repair_validations(Person) do + Person.validates_exclusion_of :karma, :in => %w( abe monkey ) + + p = Person.new + p.karma = "abe" + assert p.invalid? + + assert_equal ["is reserved"], p.errors[:karma] + + p.karma = "Lifo" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb new file mode 100644 index 0000000000..2c06a9dd02 --- /dev/null +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -0,0 +1,98 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class PresenceValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validate_format + Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") + + t = Topic.create("title" => "i'm incorrect", "content" => "Validation macros rule!") + assert !t.valid?, "Shouldn't be valid" + assert !t.save, "Shouldn't save because it's invalid" + assert_equal ["is bad data"], t.errors[:title] + assert t.errors[:content].empty? + + t.title = "Validation macros rule!" + + assert t.save + assert t.errors[:title].empty? + + assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } + end + + def test_validate_format_with_allow_blank + Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank=>true) + assert !Topic.create("title" => "Shouldn't be valid").valid? + assert Topic.create("title" => "").valid? + assert Topic.create("title" => nil).valid? + assert Topic.create("title" => "Validation macros rule!").valid? + end + + # testing ticket #3142 + def test_validate_format_numeric + Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") + + t = Topic.create("title" => "72x", "content" => "6789") + assert !t.valid?, "Shouldn't be valid" + assert !t.save, "Shouldn't save because it's invalid" + assert_equal ["is bad data"], t.errors[:title] + assert t.errors[:content].empty? + + t.title = "-11" + assert !t.valid?, "Shouldn't be valid" + + t.title = "03" + assert !t.valid?, "Shouldn't be valid" + + t.title = "z44" + assert !t.valid?, "Shouldn't be valid" + + t.title = "5v7" + assert !t.valid?, "Shouldn't be valid" + + t.title = "1" + + assert t.save + assert t.errors[:title].empty? + end + + def test_validate_format_with_formatted_message + Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be {{value}}") + t = Topic.create(:title => 'Invalid title') + assert_equal ["can't be Invalid title"], t.errors[:title] + end + + def test_validates_format_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes" + d = Developer.new + d.name = d.name_confirmation = "John 32" + assert !d.valid? + assert_equal ["format 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_format_of_for_ruby_class + repair_validations(Person) do + Person.validates_format_of :karma, :with => /\A\d+\Z/ + + p = Person.new + p.karma = "Pixies" + assert p.invalid? + + assert_equal ["is invalid"], p.errors[:karma] + + p.karma = "1234" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb new file mode 100644 index 0000000000..37bba5e95e --- /dev/null +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -0,0 +1,182 @@ +require "cases/helper" +require 'cases/tests_database' + +require 'models/person' + +class I18nGenerateMessageValidationTest < Test::Unit::TestCase + def setup + reset_callbacks Person + @person = Person.new + + @old_load_path, @old_backend = I18n.load_path, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + + I18n.backend.store_translations :'en', { + :activemodel => { + :errors => { + :messages => { + :inclusion => "is not included in the list", + :exclusion => "is reserved", + :invalid => "is invalid", + :confirmation => "doesn't match confirmation", + :accepted => "must be accepted", + :empty => "can't be empty", + :blank => "can't be blank", + :too_long => "is too long (maximum is {{count}} characters)", + :too_short => "is too short (minimum is {{count}} characters)", + :wrong_length => "is the wrong length (should be {{count}} characters)", + :not_a_number => "is not a number", + :greater_than => "must be greater than {{count}}", + :greater_than_or_equal_to => "must be greater than or equal to {{count}}", + :equal_to => "must be equal to {{count}}", + :less_than => "must be less than {{count}}", + :less_than_or_equal_to => "must be less than or equal to {{count}}", + :odd => "must be odd", + :even => "must be even" + } + } + } + } + end + + def teardown + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + def test_generate_message_inclusion_with_default_message + assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') + end + + def test_generate_message_inclusion_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + def test_generate_message_exclusion_with_default_message + assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') + end + + def test_generate_message_exclusion_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + def test_generate_message_invalid_with_default_message + assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') + end + + def test_generate_message_invalid_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) + def test_generate_message_confirmation_with_default_message + assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation, :default => nil) + end + + def test_generate_message_confirmation_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, :default => 'custom message') + end + + # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) + def test_generate_message_accepted_with_default_message + assert_equal "must be accepted", @person.errors.generate_message(:title, :accepted, :default => nil) + end + + def test_generate_message_accepted_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, :default => 'custom message') + end + + # add_on_empty: generate_message(attr, :empty, :default => custom_message) + def test_generate_message_empty_with_default_message + assert_equal "can't be empty", @person.errors.generate_message(:title, :empty, :default => nil) + end + + def test_generate_message_empty_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :empty, :default => 'custom message') + end + + # add_on_blank: generate_message(attr, :blank, :default => custom_message) + def test_generate_message_blank_with_default_message + assert_equal "can't be blank", @person.errors.generate_message(:title, :blank, :default => nil) + end + + def test_generate_message_blank_with_custom_message + assert_equal 'custom message', @person.errors.generate_message(:title, :blank, :default => 'custom message') + end + + # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + def test_generate_message_too_long_with_default_message + assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, :default => nil, :count => 10) + end + + def test_generate_message_too_long_with_custom_message + assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + def test_generate_message_too_short_with_default_message + assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, :default => nil, :count => 10) + end + + def test_generate_message_too_short_with_custom_message + assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) + def test_generate_message_wrong_length_with_default_message + assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) + end + + def test_generate_message_wrong_length_with_custom_message + assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) + end + + # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + def test_generate_message_not_a_number_with_default_message + assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') + end + + def test_generate_message_not_a_number_with_custom_message + assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + def test_generate_message_greater_than_with_default_message + assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_greater_than_or_equal_to_with_default_message + assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_equal_to_with_default_message + assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_with_default_message + assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_or_equal_to_with_default_message + assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_odd_with_default_message + assert_equal "must be odd", @person.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_even_with_default_message + assert_equal "must be even", @person.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) + end +end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb new file mode 100644 index 0000000000..a1bb93b731 --- /dev/null +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -0,0 +1,591 @@ +require "cases/helper" +require 'cases/tests_database' + +require 'models/person' + +class I18nValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + + def setup + reset_callbacks Person + + @person = Person.new + + @old_load_path, @old_backend = I18n.load_path, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations('en', :activemodel => {:errors => {:messages => {:custom => nil}}}) + end + + def teardown + reset_callbacks Person + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + def test_percent_s_interpolation_syntax_in_error_messages_still_works + ActiveSupport::Deprecation.silence do + result = I18n.t :does_not_exist, :default => "%s interpolation syntax is deprecated", :value => 'this' + assert_equal result, "this interpolation syntax is deprecated" + end + end + + def test_percent_s_interpolation_syntax_in_error_messages_is_deprecated + assert_deprecated('using %s in messages') do + I18n.t :does_not_exist, :default => "%s interpolation syntax is deprected", :value => 'this' + end + end + + def test_percent_d_interpolation_syntax_in_error_messages_still_works + ActiveSupport::Deprecation.silence do + result = I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprecated", :count => 2 + assert_equal result, "2 interpolation syntaxes are deprecated" + end + end + + def test_percent_d_interpolation_syntax_in_error_messages_is_deprecated + assert_deprecated('using %d in messages') do + I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprected", :count => 2 + end + end + + def test_errors_add_on_empty_generates_message + @person.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) + @person.errors.add_on_empty :title + end + + def test_errors_add_on_empty_generates_message_with_custom_default_message + @person.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) + @person.errors.add_on_empty :title, 'custom' + end + + def test_errors_add_on_blank_generates_message + @person.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @person.errors.add_on_blank :title + end + + def test_errors_add_on_blank_generates_message_with_custom_default_message + @person.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @person.errors.add_on_blank :title, 'custom' + end + + # ActiveRecord::Validations + # validates_confirmation_of w/ mocha + def test_validates_confirmation_of_generates_message + Person.validates_confirmation_of :title + @person.title_confirmation = 'foo' + @person.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) + @person.valid? + end + + def test_validates_confirmation_of_generates_message_with_custom_default_message + Person.validates_confirmation_of :title, :message => 'custom' + @person.title_confirmation = 'foo' + @person.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) + @person.valid? + end + + # validates_acceptance_of w/ mocha + + def test_validates_acceptance_of_generates_message + Person.validates_acceptance_of :title, :allow_nil => false + @person.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) + @person.valid? + end + + def test_validates_acceptance_of_generates_message_with_custom_default_message + Person.validates_acceptance_of :title, :message => 'custom', :allow_nil => false + @person.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) + @person.valid? + end + + # validates_presence_of w/ mocha + + def test_validates_presence_of_generates_message + Person.validates_presence_of :title + @person.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) + @person.valid? + end + + def test_validates_presence_of_generates_message_with_custom_default_message + Person.validates_presence_of :title, :message => 'custom' + @person.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short + Person.validates_length_of :title, :within => 3..5 + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long + Person.validates_length_of :title, :within => 3..5 + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @person.valid? + end + + # validates_length_of :within w/ mocha + + def test_validates_length_of_within_generates_message_with_title_too_short + Person.validates_length_of :title, :within => 3..5 + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_short => 'custom' + @person.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long + Person.validates_length_of :title, :within => 3..5 + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) + @person.valid? + end + + def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message + Person.validates_length_of :title, :within => 3..5, :too_long => 'custom' + @person.title = 'this title is too long' + @person.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) + @person.valid? + end + + # validates_length_of :is w/ mocha + + def test_validates_length_of_is_generates_message + Person.validates_length_of :title, :is => 5 + @person.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) + @person.valid? + end + + def test_validates_length_of_is_generates_message_with_custom_default_message + Person.validates_length_of :title, :is => 5, :message => 'custom' + @person.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) + @person.valid? + end + + # validates_format_of w/ mocha + + def test_validates_format_of_generates_message + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @person.title = '72x' + @person.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) + @person.valid? + end + + def test_validates_format_of_generates_message_with_custom_default_message + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' + @person.title = '72x' + @person.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) + @person.valid? + end + + # validates_inclusion_of w/ mocha + + def test_validates_inclusion_of_generates_message + Person.validates_inclusion_of :title, :in => %w(a b c) + @person.title = 'z' + @person.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) + @person.valid? + end + + def test_validates_inclusion_of_generates_message_with_custom_default_message + Person.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' + @person.title = 'z' + @person.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) + @person.valid? + end + + # validates_exclusion_of w/ mocha + + def test_validates_exclusion_of_generates_message + Person.validates_exclusion_of :title, :in => %w(a b c) + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) + @person.valid? + end + + def test_validates_exclusion_of_generates_message_with_custom_default_message + Person.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of without :only_integer w/ mocha + + def test_validates_numericality_of_generates_message + Person.validates_numericality_of :title + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :message => 'custom' + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of with :only_integer w/ mocha + + def test_validates_numericality_of_only_integer_generates_message + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :only_integer => true, :message => 'custom' + @person.title = 'a' + @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of :odd w/ mocha + + def test_validates_numericality_of_odd_generates_message + Person.validates_numericality_of :title, :only_integer => true, :odd => true + @person.title = 0 + @person.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' + @person.title = 0 + @person.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) + @person.valid? + end + + # validates_numericality_of :less_than w/ mocha + + def test_validates_numericality_of_less_than_generates_message + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @person.title = 1 + @person.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) + @person.valid? + end + + def test_validates_numericality_of_odd_generates_message_with_custom_default_message + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' + @person.title = 1 + @person.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) + @person.valid? + end + + # validates_confirmation_of w/o mocha + + def test_validates_confirmation_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:confirmation => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}} + + Person.validates_confirmation_of :title + @person.title_confirmation = 'foo' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_confirmation_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:confirmation => 'global message'}}} + + Person.validates_confirmation_of :title + @person.title_confirmation = 'foo' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_acceptance_of w/o mocha + + def test_validates_acceptance_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:accepted => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}} + + Person.validates_acceptance_of :title, :allow_nil => false + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_acceptance_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:accepted => 'global message'}}} + + Person.validates_acceptance_of :title, :allow_nil => false + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_presence_of w/o mocha + + def test_validates_presence_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:blank => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}} + + Person.validates_presence_of :title + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_presence_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:blank => 'global message'}}} + + Person.validates_presence_of :title + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_length_of :within w/o mocha + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:too_short => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}} + + Person.validates_length_of :title, :within => 3..5 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:too_short => 'global message'}}} + + Person.validates_length_of :title, :within => 3..5 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_length_of :is w/o mocha + + def test_validates_length_of_is_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_length_of_is_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + def test_validates_length_of_is_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_length_of_is_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:wrong_length => 'global message'}}} + + Person.validates_length_of :title, :is => 5 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + + # validates_format_of w/o mocha + + def test_validates_format_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:invalid => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}} + + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_format_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:invalid => 'global message'}}} + + Person.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_inclusion_of w/o mocha + + def test_validates_inclusion_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:inclusion => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}} + + Person.validates_inclusion_of :title, :in => %w(a b c) + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_inclusion_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:inclusion => 'global message'}}} + + Person.validates_inclusion_of :title, :in => %w(a b c) + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_exclusion_of w/o mocha + + def test_validates_exclusion_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:exclusion => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}} + + Person.validates_exclusion_of :title, :in => %w(a b c) + @person.title = 'a' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_exclusion_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:exclusion => 'global message'}}} + + Person.validates_exclusion_of :title, :in => %w(a b c) + @person.title = 'a' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of without :only_integer w/o mocha + + def test_validates_numericality_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title + @person.title = 'a' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of with :only_integer w/o mocha + + def test_validates_numericality_of_only_integer_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_only_integer_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:not_a_number => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true + @person.title = 'a' + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of :odd w/o mocha + + def test_validates_numericality_of_odd_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:odd => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :odd => true + @person.title = 0 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_odd_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:odd => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :odd => true + @person.title = 0 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + # validates_numericality_of :less_than w/o mocha + + def test_validates_numericality_of_less_than_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:less_than => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @person.title = 1 + @person.valid? + assert_equal ['custom message'], @person.errors[:title] + end + + def test_validates_numericality_of_less_than_finds_global_default_translation + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:less_than => 'global message'}}} + + Person.validates_numericality_of :title, :only_integer => true, :less_than => 0 + @person.title = 1 + @person.valid? + assert_equal ['global message'], @person.errors[:title] + end + + def test_validations_with_message_symbol_must_translate + I18n.backend.store_translations 'en', :activemodel => {:errors => {:messages => {:custom_error => "I am a custom error"}}} + Person.validates_presence_of :title, :message => :custom_error + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + + def test_validates_with_message_symbol_must_translate_per_attribute + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}} + Person.validates_presence_of :title, :message => :custom_error + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + + def test_validates_with_message_symbol_must_translate_per_model + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:custom_error => "I am a custom error"}}}} + Person.validates_presence_of :title, :message => :custom_error + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + + def test_validates_with_message_string + Person.validates_presence_of :title, :message => "I am a custom error" + @person.title = nil + @person.valid? + assert_equal ["I am a custom error"], @person.errors[:title] + end + +end
\ No newline at end of file diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb new file mode 100644 index 0000000000..bc1b0365d2 --- /dev/null +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -0,0 +1,80 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class InclusionValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validates_inclusion_of + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) ) + + assert !Topic.create("title" => "a!", "content" => "abc").valid? + assert !Topic.create("title" => "a b", "content" => "abc").valid? + assert !Topic.create("title" => nil, "content" => "def").valid? + + t = Topic.create("title" => "a", "content" => "I know you are but what am I?") + assert t.valid? + t.title = "uhoh" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is not included in the list"], t.errors[:title] + + assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) } + assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) } + + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) } + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) } + assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) } + end + + def test_validates_inclusion_of_with_allow_nil + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true ) + + assert !Topic.create("title" => "a!", "content" => "abc").valid? + assert !Topic.create("title" => "", "content" => "abc").valid? + assert Topic.create("title" => nil, "content" => "abc").valid? + end + + def test_validates_inclusion_of_with_formatted_message + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option {{value}} is not in the list" ) + + assert Topic.create("title" => "a", "content" => "abc").valid? + + t = Topic.create("title" => "uhoh", "content" => "abc") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["option uhoh is not in the list"], t.errors[:title] + end + + def test_validates_inclusion_of_with_custom_error_using_quotes + repair_validations(Developer) do + Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.salary = "90,000" + assert !d.valid? + assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last + end + end + + def test_validates_inclusion_of_for_ruby_class + repair_validations(Person) do + Person.validates_inclusion_of :karma, :in => %w( abe monkey ) + + p = Person.new + p.karma = "Lifo" + assert p.invalid? + + assert_equal ["is not included in the list"], p.errors[:karma] + + p.karma = "monkey" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb new file mode 100644 index 0000000000..4a2f72feab --- /dev/null +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -0,0 +1,449 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class LengthValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validates_length_of_with_allow_nil + Topic.validates_length_of( :title, :is => 5, :allow_nil=>true ) + + assert !Topic.create("title" => "ab").valid? + assert !Topic.create("title" => "").valid? + assert Topic.create("title" => nil).valid? + assert Topic.create("title" => "abcde").valid? + end + + def test_validates_length_of_with_allow_blank + Topic.validates_length_of( :title, :is => 5, :allow_blank=>true ) + + assert !Topic.create("title" => "ab").valid? + assert Topic.create("title" => "").valid? + assert Topic.create("title" => nil).valid? + assert Topic.create("title" => "abcde").valid? + end + + def test_validates_length_of_using_minimum + Topic.validates_length_of :title, :minimum => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "not" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] + + t.title = "" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] + + t.title = nil + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] + end + + def test_optionally_validates_length_of_using_minimum + Topic.validates_length_of :title, :minimum => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_maximum + Topic.validates_length_of :title, :maximum => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "notvalid" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] + + t.title = "" + assert t.valid? + + t.title = nil + assert !t.valid? + end + + def test_optionally_validates_length_of_using_maximum + Topic.validates_length_of :title, :maximum => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_within + Topic.validates_length_of(:title, :content, :within => 3..5) + + t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") + assert !t.valid? + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] + assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] + + t.title = nil + t.content = nil + assert !t.valid? + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content] + + t.title = "abe" + t.content = "mad" + assert t.valid? + end + + def test_optionally_validates_length_of_using_within + Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true + + t = Topic.create('title' => 'abc', 'content' => 'abcd') + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_optionally_validates_length_of_using_within_on_create + Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: {{count}}" + + t = Topic.create("title" => "thisisnotvalid", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + assert_equal ["my string is too long: 10"], t.errors[:title] + + t.title = "butthisis" + assert t.save + + t.title = "few" + assert t.save + + t.content = "andthisislong" + assert t.save + + t.content = t.title = "iamfine" + assert t.save + end + + def test_optionally_validates_length_of_using_within_on_update + Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: {{count}}" + + t = Topic.create("title" => "vali", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + + t.title = "not" + assert !t.save + assert t.errors[:title].any? + assert_equal ["my string is too short: 5"], t.errors[:title] + + t.title = "valid" + t.content = "andthisistoolong" + assert !t.save + assert t.errors[:content].any? + + t.content = "iamfine" + assert t.save + end + + def test_validates_length_of_using_is + Topic.validates_length_of :title, :is => 5 + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = "notvalid" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title] + + t.title = "" + assert !t.valid? + + t.title = nil + assert !t.valid? + end + + def test_optionally_validates_length_of_using_is + Topic.validates_length_of :title, :is => 5, :allow_nil => true + + t = Topic.create("title" => "valid", "content" => "whatever") + assert t.valid? + + t.title = nil + assert t.valid? + end + + def test_validates_length_of_using_bignum + bigmin = 2 ** 30 + bigmax = 2 ** 32 + bigrange = bigmin...bigmax + assert_nothing_raised do + Topic.validates_length_of :title, :is => bigmin + 5 + Topic.validates_length_of :title, :within => bigrange + Topic.validates_length_of :title, :in => bigrange + Topic.validates_length_of :title, :minimum => bigmin + Topic.validates_length_of :title, :maximum => bigmax + end + end + + def test_validates_length_of_nasty_params + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") } + end + + def test_validates_length_of_custom_errors_for_minimum_with_message + Topic.validates_length_of( :title, :minimum=>5, :message=>"boo {{count}}" ) + t = Topic.create("title" => "uhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["boo 5"], t.errors[:title] + end + + def test_validates_length_of_custom_errors_for_minimum_with_too_short + Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo {{count}}" ) + t = Topic.create("title" => "uhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors[:title] + end + + def test_validates_length_of_custom_errors_for_maximum_with_message + Topic.validates_length_of( :title, :maximum=>5, :message=>"boo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["boo 5"], t.errors[:title] + end + + def test_validates_length_of_custom_errors_for_in + Topic.validates_length_of(:title, :in => 10..20, :message => "hoo {{count}}") + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 10"], t.errors["title"] + + t = Topic.create("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 20"], t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_maximum_with_too_long + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_is_with_message + Topic.validates_length_of( :title, :is=>5, :message=>"boo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["boo 5"], t.errors["title"] + end + + def test_validates_length_of_custom_errors_for_is_with_wrong_length + Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo {{count}}" ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["hoo 5"], t.errors["title"] + end + + def test_validates_length_of_using_minimum_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :minimum => 5 + + t = Topic.create("title" => "一二三四五", "content" => "whatever") + assert t.valid? + + t.title = "一二三四" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] + end + end + + def test_validates_length_of_using_maximum_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :maximum => 5 + + t = Topic.create("title" => "一二三四五", "content" => "whatever") + assert t.valid? + + t.title = "一二34五六" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"] + end + end + + def test_validates_length_of_using_within_utf8 + with_kcode('UTF8') do + Topic.validates_length_of(:title, :content, :within => 3..5) + + t = Topic.new("title" => "一二", "content" => "12三四五六七") + assert !t.valid? + assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] + assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] + t.title = "一二三" + t.content = "12三" + assert t.valid? + end + end + + def test_optionally_validates_length_of_using_within_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :within => 3..5, :allow_nil => true + + t = Topic.create(:title => "一二三四五") + assert t.valid?, t.errors.inspect + + t = Topic.create(:title => "一二三") + assert t.valid?, t.errors.inspect + + t.title = nil + assert t.valid?, t.errors.inspect + end + end + + def test_optionally_validates_length_of_using_within_on_create_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: {{count}}" + + t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + assert_equal "長すぎます: 10", t.errors[:title].first + + t.title = "一二三四五六七八九" + assert t.save + + t.title = "一二3" + assert t.save + + t.content = "一二三四五六七八九十" + assert t.save + + t.content = t.title = "一二三四五六" + assert t.save + end + end + + def test_optionally_validates_length_of_using_within_on_update_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: {{count}}" + + t = Topic.create("title" => "一二三4", "content" => "whatever") + assert !t.save + assert t.errors[:title].any? + + t.title = "1二三4" + assert !t.save + assert t.errors[:title].any? + assert_equal ["短すぎます: 5"], t.errors[:title] + + t.title = "一二三四五六七八九十A" + assert !t.save + assert t.errors[:title].any? + + t.title = "一二345" + assert t.save + end + end + + def test_validates_length_of_using_is_utf8 + with_kcode('UTF8') do + Topic.validates_length_of :title, :is => 5 + + t = Topic.create("title" => "一二345", "content" => "whatever") + assert t.valid? + + t.title = "一二345六" + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"] + end + end + + def test_validates_length_of_with_block + Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least {{count}} words.", + :tokenizer => lambda {|str| str.scan(/\w+/) } + t = Topic.create!(:content => "this content should be long enough") + assert t.valid? + + t.content = "not long enough" + assert !t.valid? + assert t.errors[:content].any? + assert_equal ["Your essay must be at least 5 words."], t.errors[:content] + end + + def test_validates_length_of_with_custom_too_long_using_quotes + repair_validations(Developer) do + Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Jeffrey" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_length_of_with_custom_too_short_using_quotes + repair_validations(Developer) do + Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_length_of_with_custom_message_using_quotes + repair_validations(Developer) do + Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end + + def test_validates_length_of_for_ruby_class + repair_validations(Person) do + Person.validates_length_of :karma, :minimum => 5 + + p = Person.new + p.karma = "Pix" + assert p.invalid? + + assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma] + + p.karma = "The Smiths" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb new file mode 100644 index 0000000000..0af6eb69ce --- /dev/null +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -0,0 +1,179 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class NumericalityValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + NIL = [nil] + BLANK = ["", " ", " \t \r \n"] + BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits + FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5) + INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) + FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS + INTEGERS = [0, 10, -10] + INTEGER_STRINGS + BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } + JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"] + INFINITY = [1.0/0.0] + + def test_default_validates_numericality_of + Topic.validates_numericality_of :approved + invalid!(NIL + BLANK + JUNK) + valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + + def test_validates_numericality_of_with_nil_allowed + Topic.validates_numericality_of :approved, :allow_nil => true + + invalid!(JUNK) + valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + + def test_validates_numericality_of_with_integer_only + Topic.validates_numericality_of :approved, :only_integer => true + + invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY) + valid!(INTEGERS) + end + + def test_validates_numericality_of_with_integer_only_and_nil_allowed + Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true + + invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY) + valid!(NIL + BLANK + INTEGERS) + end + + def test_validates_numericality_with_greater_than + Topic.validates_numericality_of :approved, :greater_than => 10 + + invalid!([-10, 10], 'must be greater than 10') + valid!([11]) + end + + def test_validates_numericality_with_greater_than_or_equal + Topic.validates_numericality_of :approved, :greater_than_or_equal_to => 10 + + invalid!([-9, 9], 'must be greater than or equal to 10') + valid!([10]) + end + + def test_validates_numericality_with_equal_to + Topic.validates_numericality_of :approved, :equal_to => 10 + + invalid!([-10, 11] + INFINITY, 'must be equal to 10') + valid!([10]) + end + + def test_validates_numericality_with_less_than + Topic.validates_numericality_of :approved, :less_than => 10 + + invalid!([10], 'must be less than 10') + valid!([-9, 9]) + end + + def test_validates_numericality_with_less_than_or_equal_to + Topic.validates_numericality_of :approved, :less_than_or_equal_to => 10 + + invalid!([11], 'must be less than or equal to 10') + valid!([-10, 10]) + end + + def test_validates_numericality_with_odd + Topic.validates_numericality_of :approved, :odd => true + + invalid!([-2, 2], 'must be odd') + valid!([-1, 1]) + end + + def test_validates_numericality_with_even + Topic.validates_numericality_of :approved, :even => true + + invalid!([-1, 1], 'must be even') + valid!([-2, 2]) + end + + def test_validates_numericality_with_greater_than_less_than_and_even + Topic.validates_numericality_of :approved, :greater_than => 1, :less_than => 4, :even => true + + invalid!([1, 3, 4]) + valid!([2]) + end + + def test_validates_numericality_with_numeric_message + Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than {{count}}" + topic = Topic.new("title" => "numeric test", "approved" => 10) + + assert !topic.valid? + assert_equal ["smaller than 4"], topic.errors[:approved] + + Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than {{count}}" + topic = Topic.new("title" => "numeric test", "approved" => 1) + + assert !topic.valid? + assert_equal ["greater than 4"], topic.errors[:approved] + end + + def test_numericality_with_getter_method + repair_validations(Developer) do + Developer.validates_numericality_of( :salary ) + developer = Developer.new("name" => "michael", "salary" => nil) + developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") + assert developer.valid? + end + end + + def test_numericality_with_allow_nil_and_getter_method + repair_validations(Developer) do + Developer.validates_numericality_of( :salary, :allow_nil => true) + developer = Developer.new("name" => "michael", "salary" => nil) + developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") + assert developer.valid? + end + end + + def test_validates_numericality_of_for_ruby_class + repair_validations(Person) do + Person.validates_numericality_of :karma, :allow_nil => false + + p = Person.new + p.karma = "Pix" + assert p.invalid? + + assert_equal ["is not a number"], p.errors[:karma] + + p.karma = "1234" + assert p.valid? + end + end + + private + + def invalid!(values, error = nil) + with_each_topic_approved_value(values) do |topic, value| + assert !topic.valid?, "#{value.inspect} not rejected as a number" + assert topic.errors[:approved].any?, "FAILED for #{value.inspect}" + assert_equal error, topic.errors[:approved].first if error + end + end + + def valid!(values) + with_each_topic_approved_value(values) do |topic, value| + assert topic.valid?, "#{value.inspect} not accepted as a number" + end + end + + def with_each_topic_approved_value(values) + topic = Topic.new(:title => "numeric test", :content => "whatever") + values.each do |value| + topic.approved = value + yield topic, value + end + end +end diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb new file mode 100644 index 0000000000..f6bed4903a --- /dev/null +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -0,0 +1,57 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/developer' +require 'models/person' + +class PresenceValidationTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + repair_validations(Topic) + + def test_validate_presences + Topic.validates_presence_of(:title, :content) + + t = Topic.create + assert !t.save + assert_equal ["can't be blank"], t.errors[:title] + assert_equal ["can't be blank"], t.errors[:content] + + t.title = "something" + t.content = " " + + assert !t.save + assert_equal ["can't be blank"], t.errors[:content] + + t.content = "like stuff" + + assert t.save + end + + def test_validates_presence_of_with_custom_message_using_quotes + repair_validations(Developer) do + Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "Joe" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:non_existent] + end + end + + def test_validates_presence_of_for_ruby_class + repair_validations(Person) do + Person.validates_presence_of :karma + + p = Person.new + assert p.invalid? + + assert_equal ["can't be blank"], p.errors[:karma] + + p.karma = "Cold" + assert p.valid? + end + end +end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb new file mode 100644 index 0000000000..8c89494247 --- /dev/null +++ b/activemodel/test/cases/validations_test.rb @@ -0,0 +1,178 @@ +# encoding: utf-8 +require 'cases/helper' +require 'cases/tests_database' + +require 'models/topic' +require 'models/reply' +require 'models/developer' + +class ValidationsTest < ActiveModel::TestCase + include ActiveModel::TestsDatabase + include ActiveModel::ValidationsRepairHelper + + # Most of the tests mess with the validations of Topic, so lets repair it all the time. + # Other classes we mess with will be dealt with in the specific tests + repair_validations(Topic) + + def test_single_field_validation + r = Reply.new + r.title = "There's no content!" + assert !r.valid?, "A reply without content shouldn't be saveable" + + r.content = "Messa content!" + assert r.valid?, "A reply with content should be saveable" + end + + def test_single_attr_validation_and_error_msg + r = Reply.new + r.title = "There's no content!" + assert !r.valid? + assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid" + assert_equal ["Empty"], r.errors["content"], "A reply without content should contain an error" + assert_equal 1, r.errors.count + end + + def test_double_attr_validation_and_error_msg + r = Reply.new + assert !r.valid? + + assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid" + assert_equal ["Empty"], r.errors["title"], "A reply without title should contain an error" + + assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid" + assert_equal ["Empty"], r.errors["content"], "A reply without content should contain an error" + + assert_equal 2, r.errors.count + end + + def test_single_error_per_attr_iteration + r = Reply.new + r.save + + errors = [] + r.errors.each {|attr, messages| errors << [attr.to_s, messages] } + + assert errors.include?(["title", "Empty"]) + assert errors.include?(["content", "Empty"]) + end + + def test_multiple_errors_per_attr_iteration_with_full_error_composition + r = Reply.new + r.title = "Wrong Create" + r.content = "Mismatch" + r.save + + errors = r.errors.to_a + + assert_equal "Title is Wrong Create", errors[0] + assert_equal "Title is Content Mismatch", errors[1] + assert_equal 2, r.errors.count + end + + def test_errors_on_base + r = Reply.new + r.content = "Mismatch" + r.save + r.errors[:base] << "Reply is not dignifying" + + errors = [] + r.errors.to_a.each { |error| errors << error } + + assert_equal ["Reply is not dignifying"], r.errors[:base] + + assert errors.include?("Title Empty") + assert errors.include?("Reply is not dignifying") + assert_equal 2, r.errors.count + end + + def test_validates_each + hits = 0 + Topic.validates_each(:title, :content, [:title, :content]) do |record, attr| + record.errors.add attr, 'gotcha' + hits += 1 + end + t = Topic.new("title" => "valid", "content" => "whatever") + assert !t.save + assert_equal 4, hits + assert_equal %w(gotcha gotcha), t.errors[:title] + assert_equal %w(gotcha gotcha), t.errors[:content] + end + + def test_validate_block + Topic.validate { |topic| topic.errors.add("title", "will never be valid") } + t = Topic.create("title" => "Title", "content" => "whatever") + assert !t.valid? + assert t.errors[:title].any? + assert_equal ["will never be valid"], t.errors["title"] + end + + def test_invalid_validator + Topic.validate 3 + assert_raise(ArgumentError) { t = Topic.create } + end + + def test_errors_to_xml + r = Reply.new :title => "Wrong Create" + assert !r.valid? + xml = r.errors.to_xml(:skip_instruct => true) + assert_equal "<errors>", xml.first(8) + assert xml.include?("<error>Title is Wrong Create</error>") + assert xml.include?("<error>Content Empty</error>") + end + + def test_validation_order + Topic.validates_presence_of :title + Topic.validates_length_of :title, :minimum => 2 + + t = Topic.new("title" => "") + assert !t.valid? + assert_equal "can't be blank", t.errors["title"].first + end + + def test_invalid_should_be_the_opposite_of_valid + Topic.validates_presence_of :title + + t = Topic.new + assert t.invalid? + assert t.errors[:title].any? + + t.title = 'Things are going to change' + assert !t.invalid? + end + + def test_deprecated_error_messages_on + Topic.validates_presence_of :title + + t = Topic.new + assert t.invalid? + + [:title, "title"].each do |attribute| + assert_deprecated { assert_equal "can't be blank", t.errors.on(attribute) } + end + + Topic.validates_each(:title) do |record, attribute| + record.errors[attribute] << "invalid" + end + + assert t.invalid? + + [:title, "title"].each do |attribute| + assert_deprecated do + assert t.errors.on(attribute).include?("invalid") + assert t.errors.on(attribute).include?("can't be blank") + end + end + end + + def test_deprecated_errors_on_base_and_each + t = Topic.new + assert t.valid? + + assert_deprecated { t.errors.add_to_base "invalid topic" } + assert_deprecated { assert_equal "invalid topic", t.errors.on_base } + assert_deprecated { assert t.errors.invalid?(:base) } + + all_errors = t.errors.to_a + assert_deprecated { assert_equal all_errors, t.errors.each_full{|err| err} } + end +end
\ No newline at end of file diff --git a/activemodel/test/config.rb b/activemodel/test/config.rb new file mode 100644 index 0000000000..0b577a9936 --- /dev/null +++ b/activemodel/test/config.rb @@ -0,0 +1,3 @@ +TEST_ROOT = File.expand_path(File.dirname(__FILE__)) +FIXTURES_ROOT = TEST_ROOT + "/fixtures" +SCHEMA_FILE = TEST_ROOT + "/schema.rb" diff --git a/activemodel/test/fixtures/topics.yml b/activemodel/test/fixtures/topics.yml new file mode 100644 index 0000000000..e4c61ce2d8 --- /dev/null +++ b/activemodel/test/fixtures/topics.yml @@ -0,0 +1,41 @@ +first: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: 2003-07-16t15:28:11.2233+01:00 + last_read: 2004-04-15 + bonus_time: 2005-01-30t15:28:00.00+01:00 + content: Have a nice day + approved: false + replies_count: 1 + +second: + id: 2 + title: The Second Topic of the day + author_name: Mary + written_on: 2004-07-15t15:28:00.0099+01:00 + content: Have a nice day + approved: true + replies_count: 0 + parent_id: 1 + type: Reply + +third: + id: 3 + title: The Third Topic of the day + author_name: Nick + written_on: 2005-07-15t15:28:00.0099+01:00 + content: I'm a troll + approved: true + replies_count: 1 + +fourth: + id: 4 + title: The Fourth Topic of the day + author_name: Carl + written_on: 2006-07-15t15:28:00.0099+01:00 + content: Why not? + approved: true + type: Reply + parent_id: 3 diff --git a/activemodel/test/models/developer.rb b/activemodel/test/models/developer.rb new file mode 100644 index 0000000000..5e6eefeed1 --- /dev/null +++ b/activemodel/test/models/developer.rb @@ -0,0 +1,6 @@ +class Developer < ActiveRecord::Base + validates_inclusion_of :salary, :in => 50000..200000 + validates_length_of :name, :within => 3..20 + + attr_accessor :name_confirmation +end diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb new file mode 100644 index 0000000000..d98420f900 --- /dev/null +++ b/activemodel/test/models/person.rb @@ -0,0 +1,5 @@ +class Person + include ActiveModel::Validations + + attr_accessor :title, :karma +end diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb new file mode 100644 index 0000000000..acfd801674 --- /dev/null +++ b/activemodel/test/models/reply.rb @@ -0,0 +1,34 @@ +require 'models/topic' + +class Reply < Topic + validate :errors_on_empty_content + validate_on_create :title_is_wrong_create + + validate :check_empty_title + validate_on_create :check_content_mismatch + validate_on_update :check_wrong_update + + attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read + + def check_empty_title + errors[:title] << "Empty" unless attribute_present?("title") + end + + def errors_on_empty_content + errors[:content] << "Empty" unless attribute_present?("content") + end + + def check_content_mismatch + if attribute_present?("title") && attribute_present?("content") && content == "Mismatch" + errors[:title] << "is Content Mismatch" + end + end + + def title_is_wrong_create + errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create" + end + + def check_wrong_update + errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update" + end +end diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb new file mode 100644 index 0000000000..1350aa17e7 --- /dev/null +++ b/activemodel/test/models/topic.rb @@ -0,0 +1,9 @@ +class Topic < ActiveRecord::Base + def condition_is_true + true + end + + def condition_is_true_but_its_not + false + end +end diff --git a/activemodel/test/schema.rb b/activemodel/test/schema.rb new file mode 100644 index 0000000000..56b824d445 --- /dev/null +++ b/activemodel/test/schema.rb @@ -0,0 +1,14 @@ +ActiveRecord::Schema.define do + create_table :topics, :force => true do |t| + t.string :title + t.string :author_name + t.text :content + t.boolean :approved, :default => true + t.string :type + end + + create_table :developers, :force => true do |t| + t.string :name + t.float :salary + end +end diff --git a/activemodel/test/test_helper.rb b/activemodel/test/test_helper.rb deleted file mode 100644 index 5b5678e42d..0000000000 --- a/activemodel/test/test_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rubygems' -require 'test/unit' - -gem 'mocha', '>= 0.9.3' -require 'mocha' - -require 'active_model' -require 'active_model/state_machine' - -$:.unshift File.dirname(__FILE__) + "/../../activesupport/lib" -require 'active_support' -require 'active_support/test_case' - -class ActiveModel::TestCase < ActiveSupport::TestCase -end - -begin - require 'ruby-debug' - Debugger.start -rescue LoadError -end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 2f8c5c712f..07a10fb023 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -31,6 +31,13 @@ rescue LoadError end end +begin + require 'active_model' +rescue LoadError + $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" + require 'active_model' +end + module ActiveRecord # TODO: Review explicit loads to see if they will automatically be handled by the initilizer. def self.load_all! diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 741aa2acbe..4a03585b18 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -251,7 +251,7 @@ module ActiveRecord unless association.marked_for_destruction? association.errors.each do |attribute, message| attribute = "#{reflection.name}_#{attribute}" - errors.add(attribute, message) unless errors.on(attribute) + errors[attribute] << message if errors[attribute].empty? end end else diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index d2d12b80c9..744205df1b 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -14,59 +14,46 @@ module ActiveRecord end end - # Active Record validation is reported to and from this object, which is used by Base#save to - # determine whether the object is in a valid state to be saved. See usage example in Validations. - class Errors - include Enumerable - + class Errors < ActiveModel::Errors class << self def default_error_messages - ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').") + message = "Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages')." + ActiveSupport::Deprecation.warn(message) + I18n.translate 'activerecord.errors.messages' end end - def initialize(base) # :nodoc: - @base, @errors = base, {} - end - - # Adds an error to the base object instead of any particular attribute. This is used - # to report errors that don't tie to any specific attribute, but rather to the object - # as a whole. These error messages don't get prepended with any field name when iterating - # with +each_full+, so they should be complete sentences. - def add_to_base(msg) - add(:base, msg) - end + # Returns all the full error messages in an array. + # + # class Company < ActiveRecord::Base + # validates_presence_of :name, :address, :email + # validates_length_of :name, :in => 5..30 + # end + # + # company = Company.create(:address => '123 First St.') + # company.errors.full_messages # => + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + def full_messages(options = {}) + full_messages = [] - # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> - # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one - # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. - # If no +messsage+ is supplied, :invalid is assumed. - # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). - def add(attribute, message = nil, options = {}) - message ||= :invalid - message = generate_message(attribute, message, options) if message.is_a?(Symbol) - @errors[attribute.to_s] ||= [] - @errors[attribute.to_s] << message - end + each do |attribute, messages| + next if messages.empty? - # Will add an error message to each of the attributes in +attributes+ that is empty. - def add_on_empty(attributes, custom_message = nil) - for attr in [attributes].flatten - value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - is_empty = value.respond_to?(:empty?) ? value.empty? : false - add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty + if attribute == :base + messages.each {|m| full_messages << m } + else + attr_name = @base.class.human_attribute_name(attribute.to_s) + prefix = attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + messages.each do |m| + full_messages << "#{prefix}#{m}" + end + end end - end - # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). - def add_on_blank(attributes, custom_message = nil) - for attr in [attributes].flatten - value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - add(attr, :blank, :default => custom_message) if value.blank? - end + full_messages end - + # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>). # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the @@ -86,7 +73,6 @@ module ActiveRecord # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li> # </ol> def generate_message(attribute, message = :invalid, options = {}) - message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) defaults = @base.class.self_and_descendants_from_active_record.map do |klass| @@ -109,876 +95,23 @@ module ActiveRecord I18n.translate(key, options) end - - # Returns true if the specified +attribute+ has errors associated with it. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.invalid?(:name) # => true - # company.errors.invalid?(:address) # => false - def invalid?(attribute) - !@errors[attribute.to_s].nil? - end - - # Returns +nil+, if no errors are associated with the specified +attribute+. - # Returns the error message, if one error is associated with the specified +attribute+. - # Returns an array of error messages, if more than one error is associated with the specified +attribute+. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"] - # company.errors.on(:email) # => "can't be blank" - # company.errors.on(:address) # => nil - def on(attribute) - errors = @errors[attribute.to_s] - return nil if errors.nil? - errors.size == 1 ? errors.first : errors - end - - alias :[] :on - - # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>. - def on_base - on(:base) - end - - # Yields each attribute and associated message per error added. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } - # # => name - is too short (minimum is 5 characters) - # # name - can't be blank - # # address - can't be blank - def each - @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } - end - - # Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned - # through iteration as "First name can't be empty". - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.each_full{|msg| puts msg } - # # => Name is too short (minimum is 5 characters) - # # Name can't be blank - # # Address can't be blank - def each_full - full_messages.each { |msg| yield msg } - end - - # Returns all the full error messages in an array. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] - def full_messages(options = {}) - full_messages = [] - - @errors.each_key do |attr| - @errors[attr].each do |message| - next unless message - - if attr == "base" - full_messages << message - else - attr_name = @base.class.human_attribute_name(attr) - full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message - end - end - end - full_messages - end - - # Returns true if no errors have been added. - def empty? - @errors.empty? - end - - # Removes all errors that have been added. - def clear - @errors = {} - end - - # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. - def size - @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } - end - - alias_method :count, :size - alias_method :length, :size - - # Returns an XML representation of this error object. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # company = Company.create(:address => '123 First St.') - # company.errors.to_xml - # # => <?xml version="1.0" encoding="UTF-8"?> - # # <errors> - # # <error>Name is too short (minimum is 5 characters)</error> - # # <error>Name can't be blank</error> - # # <error>Address can't be blank</error> - # # </errors> - def to_xml(options={}) - options[:root] ||= "errors" - options[:indent] ||= 2 - options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) - - options[:builder].instruct! unless options.delete(:skip_instruct) - options[:builder].errors do |e| - full_messages.each { |msg| e.error(msg) } - end - end - end - - # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. - # - # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and - # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring - # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). - # - # Example: - # - # class Person < ActiveRecord::Base - # protected - # def validate - # errors.add_on_empty %w( first_name last_name ) - # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/ - # end - # - # def validate_on_create # is only run the first time a new object is saved - # unless valid_discount?(membership_discount) - # errors.add("membership_discount", "has expired") - # end - # end - # - # def validate_on_update - # errors.add_to_base("No changes have occurred") if unchanged_attributes? - # end - # end - # - # person = Person.new("first_name" => "David", "phone_number" => "what?") - # person.save # => false (and doesn't do the save) - # person.errors.empty? # => false - # person.errors.count # => 2 - # person.errors.on "last_name" # => "can't be empty" - # person.errors.on "phone_number" # => "has invalid format" - # person.errors.each_full { |msg| puts msg } - # # => "Last name can't be empty\n" + - # # "Phone number has invalid format" - # - # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" } - # person.save # => true (and person is now saved in the database) - # - # An Errors object is automatically created for every Active Record. module Validations - VALIDATIONS = %w( validate validate_on_create validate_on_update ) - def self.included(base) # :nodoc: + base.send :include, ActiveModel::Validations base.extend ClassMethods + base.send :include, InstanceMethods + + base.define_callbacks :validate_on_create, :validate_on_update + base.class_eval do alias_method_chain :save, :validation alias_method_chain :save!, :validation end - - base.send :include, ActiveSupport::Callbacks - base.define_callbacks *VALIDATIONS end - # Active Record classes can implement validations in several ways. The highest level, easiest to read, - # and recommended approach is to use the declarative <tt>validates_..._of</tt> class methods (and - # +validates_associated+) documented below. These are sufficient for most model validations. - # - # Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative - # validation methods, but like all the lower-level approaches it requires manually adding to the errors collection - # when the record is invalid. - # - # At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+ - # to add validation methods or blocks. These are ActiveSupport::Callbacks and follow the same rules of inheritance - # and chaining. - # - # The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+ - # as documented in ActiveRecord::Validations. - # - # == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods - # - # Calls to these methods add a validation method or block to the class. Again, this approach is recommended - # only when the higher-level methods documented below (<tt>validates_..._of</tt> and +validates_associated+) are - # insufficient to handle the required validation. - # - # This can be done with a symbol pointing to a method: - # - # class Comment < ActiveRecord::Base - # validate :must_be_friends - # - # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) - # end - # end - # - # Or with a block which is passed the current record to be validated: - # - # class Comment < ActiveRecord::Base - # validate do |comment| - # comment.must_be_friends - # end - # - # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) - # end - # end - # - # This usage applies to +validate_on_create+ and +validate_on_update+ as well. module ClassMethods - DEFAULT_VALIDATION_OPTIONS = { - :on => :save, - :allow_nil => false, - :allow_blank => false, - :message => nil - }.freeze - - ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze - ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', - :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', - :odd => 'odd?', :even => 'even?' }.freeze - - # Validates each attribute against a block. - # - # class Person < ActiveRecord::Base - # validates_each :first_name, :last_name do |record, attr, value| - # record.errors.add attr, 'starts with z.' if value[0] == ?z - # end - # end - # - # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. - # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_each(*attrs) - options = attrs.extract_options!.symbolize_keys - attrs = attrs.flatten - - # Declare the validation. - send(validation_method(options[:on] || :save), options) do |record| - attrs.each do |attr| - value = record.send(attr) - next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) - 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: - # class Person < ActiveRecord::Base - # validates_confirmation_of :user_name, :password - # validates_confirmation_of :email_address, :message => "should match confirmation" - # end - # - # View: - # <%= password_field "person", "password" %> - # <%= password_field "person", "password_confirmation" %> - # - # The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password. - # To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed - # only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence - # check for the confirmation attribute: - # - # validates_presence_of :password_confirmation, :if => :password_changed? - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_confirmation_of(*attr_names) - 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| - 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 - - # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: - # - # class Person < ActiveRecord::Base - # validates_acceptance_of :terms_of_service - # 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. - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "must be accepted"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true). - # * <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. This should be set to +true+ if you are validating a database - # column, since the attribute is typecast from "1" to +true+ before validation. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_acceptance_of(*attr_names) - configuration = { :on => :save, :allow_nil => true, :accept => "1" } - configuration.update(attr_names.extract_options!) - - db_cols = begin - column_names - 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| - unless value == configuration[:accept] - record.errors.add(attr_name, :accepted, :default => configuration[:message]) - end - end - end - - # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: - # - # class Person < ActiveRecord::Base - # validates_presence_of :first_name - # end - # - # 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 <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. - # - # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. - # - # Configuration options: - # * <tt>message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, - # <tt>:update</tt>). - # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # - def validates_presence_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - # can't use validates_each here, because it cannot cope with nonexistent attributes, - # while errors.add_on_empty can - send(validation_method(configuration[:on]), configuration) do |record| - record.errors.add_on_blank(attr_names, configuration[:message]) - 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 - # 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 {{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: - # * <tt>:minimum</tt> - The minimum size of the attribute. - # * <tt>:maximum</tt> - The maximum size of the attribute. - # * <tt>:is</tt> - The exact size of the attribute. - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. - # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. - # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. - # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)"). - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)"). - # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)"). - # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to - # count words as in above example.) - # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. - def validates_length_of(*attrs) - # Merge given options with defaults. - options = { - :tokenizer => lambda {|value| value.split(//)} - }.merge(DEFAULT_VALIDATION_OPTIONS) - options.update(attrs.extract_options!.symbolize_keys) - - # Ensure that one and only one range option is specified. - range_options = ALL_RANGE_OPTIONS & options.keys - case range_options.size - when 0 - raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' - when 1 - # Valid number of options; do nothing. - else - raise ArgumentError, 'Too many range options specified. Choose only one.' - end - - # 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) - - 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 - 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 => "<=" } - - 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 - - alias_method :validates_size_of, :validates_length_of - - - # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user - # can be named "davidhh". - # - # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id - # end - # - # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, - # making sure that a teacher can only be on the schedule once per semester for a particular class. - # - # class TeacherSchedule < ActiveRecord::Base - # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] - # end - # - # 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. - # - # Configuration options: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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 = { :case_sensitive => true } - configuration.update(attr_names.extract_options!) - - validates_each(attr_names,configuration) do |record, attr_name, value| - # The check for an existing value should be run from a class that - # isn't abstract. This means working down from the current class - # (self), to the first non-abstract class. Since classes don't know - # their subclasses, we have to build the hierarchy between self and - # the record's class. - class_hierarchy = [record.class] - while class_hierarchy.first != self - class_hierarchy.insert(0, class_hierarchy.first.superclass) - end - - # Now we can work our way down the tree to the first non-abstract - # class (which has a database table to query from). - finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - - 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 - 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 " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) - condition_params << scope_value - end - end - - unless record.new_record? - condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" - condition_params << record.send(:id) - end - - 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 - end - end - end - - - # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression - # provided. - # - # class Person < ActiveRecord::Base - # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create - # end - # - # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. - # - # A regular expression must be provided or else an exception will be raised. - # - # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <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 <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_format_of(*attr_names) - 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| - unless value.to_s =~ configuration[:with] - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) - end - end - end - - # Validates whether the value of the specified attribute is available in a particular enumerable object. - # - # 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 {{value}} is not included in the list" - # end - # - # Configuration options: - # * <tt>:in</tt> - An enumerable object of available items. - # * <tt>:message</tt> - Specifies a custom 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 +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_inclusion_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - enum = configuration[:in] || configuration[:within] - - 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| - unless enum.include?(value) - record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) - end - end - end - - # Validates that the value of the specified attribute is not in a particular enumerable object. - # - # 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 {{value}} is not allowed" - # end - # - # Configuration options: - # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. - # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_exclusion_of(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - enum = configuration[:in] || configuration[:within] - - 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| - if enum.include?(value) - record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) - end - end - end - - # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. - # - # class Book < ActiveRecord::Base - # has_many :pages - # belongs_to :library - # - # validates_associated :pages, :library - # end - # - # Warning: If, after the above definition, you then wrote: - # - # class Page < ActiveRecord::Base - # belongs_to :book - # - # validates_associated :book - # end - # - # 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: - # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_associated(*attr_names) - configuration = { :on => :save } - configuration.update(attr_names.extract_options!) - - validates_each(attr_names, configuration) do |record, attr_name, value| - 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 - - # Validates whether the value of the specified attribute is numeric by trying to convert it to - # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_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 <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). - # * <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 columns empty strings are converted to +nil+. - # * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value. - # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value. - # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value. - # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value. - # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value. - # * <tt>:odd</tt> - Specifies the value must be an odd number. - # * <tt>:even</tt> - Specifies the value must be an even number. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - def validates_numericality_of(*attr_names) - configuration = { :on => :save, :only_integer => false, :allow_nil => false } - configuration.update(attr_names.extract_options!) - - - numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys - - (numericality_options - [ :odd, :even ]).each do |option| - raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) - end - - validates_each(attr_names,configuration) do |record, attr_name, value| - raw_value = record.send("#{attr_name}_before_type_cast") || value - - next if configuration[:allow_nil] and raw_value.nil? - - if configuration[:only_integer] - unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - 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) - rescue ArgumentError, TypeError - record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - next - end - end - - numericality_options.each do |option| - case option - when :odd, :even - unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] - record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) - end - else - record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] - end - end - end - end - # Creates an object just like Base.create but calls save! instead of save # so an exception is raised if the record is invalid. def create!(attributes = nil, &block) @@ -992,75 +125,83 @@ module ActiveRecord end end - private - def validation_method(on) - case on - when :save then :validate - when :create then :validate_on_create - when :update then :validate_on_update - end + def validation_method(on) + case on + when :create + :validate_on_create + when :update + :validate_on_update + else + :validate end + end end - # The validation process on save can be skipped by passing false. The regular Base#save method is - # replaced with this when the validations module is mixed in, which it is by default. - def save_with_validation(perform_validation = true) - if perform_validation && valid? || !perform_validation - save_without_validation - else - false + module InstanceMethods + # The validation process on save can be skipped by passing false. The regular Base#save method is + # replaced with this when the validations module is mixed in, which it is by default. + def save_with_validation(perform_validation = true) + if perform_validation && valid? || !perform_validation + save_without_validation + else + false + end end - end - # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false - # if the record is not valid. - def save_with_validation! - if valid? - save_without_validation! - else - raise RecordInvalid.new(self) + # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false + # if the record is not valid. + def save_with_validation! + if valid? + save_without_validation! + else + raise RecordInvalid.new(self) + end end - end - # Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false. - def valid? - errors.clear + # Runs all the specified validations and returns true if no errors were added otherwise false. + def valid? + errors.clear - run_callbacks(:validate) - validate + run_callbacks(:validate) - if new_record? - run_callbacks(:validate_on_create) - validate_on_create - else - run_callbacks(:validate_on_update) - validate_on_update - end + if respond_to?(:validate) + ActiveSupport::Deprecation.warn("Base#validate has been deprecated, please use Base.validate :method instead") + validate + end - errors.empty? - end + if new_record? + run_callbacks(:validate_on_create) - # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise. - def invalid? - !valid? - end + if respond_to?(:validate_on_create) + ActiveSupport::Deprecation.warn("Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead") + validate_on_create + end + else + run_callbacks(:validate_on_update) - # Returns the Errors object that holds all information about attribute error messages. - def errors - @errors ||= Errors.new(self) - end + if respond_to?(:validate_on_update) + ActiveSupport::Deprecation.warn("Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead") + validate_on_update + end + end - protected - # Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes. - def validate + errors.empty? end - # Overwrite this method for validation checks used only on creation. - def validate_on_create + # Returns the Errors object that holds all information about attribute error messages. + def errors + @errors ||= Errors.new(self) end - # Overwrite this method for validation checks used only on updates. - def validate_on_update + def get_attribute_value(attribute) + respond_to?(attribute.to_sym) ? send(attribute.to_sym) : self[attribute.to_sym] end + end + end end + +Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| + filename = File.basename(path) + require "active_record/validations/#{filename}" +end diff --git a/activemodel/lib/active_model/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b2d78af580..92f47d770f 100644 --- a/activemodel/lib/active_model/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,4 +1,4 @@ -module ActiveModel +module ActiveRecord module Validations module ClassMethods # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. @@ -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: # * <tt>:message</tt> - A custom error message (default is: "is invalid") - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>) + # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. @@ -33,12 +33,12 @@ module ActiveModel # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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.extract_options!) + configuration = 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/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb new file mode 100644 index 0000000000..edec4e9e43 --- /dev/null +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -0,0 +1,159 @@ +module ActiveRecord + module Validations + module ClassMethods + # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user + # can be named "davidhh". + # + # class Person < ActiveRecord::Base + # validates_uniqueness_of :user_name, :scope => :account_id + # end + # + # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example, + # making sure that a teacher can only be on the schedule once per semester for a particular class. + # + # class TeacherSchedule < ActiveRecord::Base + # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] + # end + # + # 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. + # + # Configuration options: + # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). + # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). 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 = { :case_sensitive => true } + configuration.update(attr_names.extract_options!) + + validates_each(attr_names,configuration) do |record, attr_name, value| + # The check for an existing value should be run from a class that + # isn't abstract. This means working down from the current class + # (self), to the first non-abstract class. Since classes don't know + # their subclasses, we have to build the hierarchy between self and + # the record's class. + class_hierarchy = [record.class] + while class_hierarchy.first != self + class_hierarchy.insert(0, class_hierarchy.first.superclass) + end + + # Now we can work our way down the tree to the first non-abstract + # class (which has a database table to query from). + finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } + + 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 + 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 " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) + condition_params << scope_value + end + end + + unless record.new_record? + condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" + condition_params << record.send(:id) + end + + 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 + end + end + end + end + end +end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 1ddb3f49bf..7140de77ea 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -183,7 +183,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase account = firm.build_account assert !account.save - assert_equal "can't be empty", account.errors.on("credit_limit") + assert_equal ["can't be empty"], account.errors["credit_limit"] end def test_build_association_twice_without_saving_affects_nothing @@ -219,7 +219,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.account assert !account.save assert_equal account, firm.account - assert_equal "can't be empty", account.errors.on("credit_limit") + assert_equal ["can't be empty"], account.errors["credit_limit"] end def test_create diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 436f50d395..2712b2a05c 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -47,7 +47,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert !firm.account.valid? assert !firm.valid? assert !firm.save - assert_equal "is invalid", firm.errors.on("account") + assert_equal ["is invalid"], firm.errors["account"] end def test_save_succeeds_for_invalid_has_one_with_validate_false @@ -133,7 +133,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert !log.developer.valid? assert !log.valid? assert !log.save - assert_equal "is invalid", log.errors.on("developer") + assert_equal ["is invalid"], log.errors["developer"] end def test_save_succeeds_for_invalid_belongs_to_with_validate_false @@ -623,16 +623,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @pirate.ship.name = '' - assert !@pirate.valid? - assert !@pirate.errors.on(:ship_name).blank? + assert @pirate.invalid? + assert @pirate.errors[:ship_name].any? end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid @pirate.ship.name = nil @pirate.catchphrase = nil - assert !@pirate.valid? - assert !@pirate.errors.on(:ship_name).blank? - assert !@pirate.errors.on(:catchphrase).blank? + assert @pirate.invalid? + assert @pirate.errors[:ship_name].any? + assert @pirate.errors[:catchphrase].any? end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -713,16 +713,16 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase def test_should_automatically_validate_the_associated_model @ship.pirate.catchphrase = '' - assert !@ship.valid? - assert !@ship.errors.on(:pirate_catchphrase).blank? + assert @ship.invalid? + assert @ship.errors[:pirate_catchphrase].any? end def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid @ship.name = nil @ship.pirate.catchphrase = nil - assert !@ship.valid? - assert !@ship.errors.on(:name).blank? - assert !@ship.errors.on(:pirate_catchphrase).blank? + assert @ship.invalid? + assert @ship.errors[:name].any? + assert @ship.errors[:pirate_catchphrase].any? end def test_should_still_allow_to_bypass_validations_on_the_associated_model @@ -784,16 +784,16 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each { |child| child.name = '' } assert !@pirate.valid? - assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") - assert @pirate.errors.on(@association_name).blank? + assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"] + assert @pirate.errors[@association_name].empty? end def test_should_not_use_default_invalid_error_on_associated_models @pirate.send(@association_name).build(:name => '') assert !@pirate.valid? - assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") - assert @pirate.errors.on(@association_name).blank? + assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"] + assert @pirate.errors[@association_name].empty? end def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid @@ -801,8 +801,8 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.catchphrase = nil assert !@pirate.valid? - assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") - assert !@pirate.errors.on(:catchphrase).blank? + assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"] + assert @pirate.errors[:catchphrase].any? end def test_should_allow_to_bypass_validations_on_the_associated_models_on_update diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 1ec52ac24d..a4f05f0c72 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -57,7 +57,7 @@ end class ActiveSupport::TestCase include ActiveRecord::TestFixtures - include ActiveRecord::Testing::RepairHelper + include ActiveModel::ValidationsRepairHelper self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb new file mode 100644 index 0000000000..b1203c12ed --- /dev/null +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -0,0 +1,101 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/topic' +require 'models/reply' +require 'models/owner' + +class AssociationValidationTest < ActiveRecord::TestCase + fixtures :topics, :owners + + repair_validations(Topic) + + def test_validates_size_of_association + repair_validations(Owner) do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + pet = o.pets.build('name' => 'apet') + assert o.valid? + end + end + + def test_validates_size_of_association_using_within + repair_validations(Owner) do + assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + + pet = o.pets.build('name' => 'apet') + assert o.valid? + + 2.times { o.pets.build('name' => 'apet') } + assert !o.save + assert o.errors[:pets].any? + end + end + + def test_validates_associated_many + Topic.validates_associated( :replies ) + t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")] + assert !t.valid? + assert t.errors[:replies].any? + assert_equal 1, r.errors.count # make sure all associated objects have been validated + assert_equal 0, r2.errors.count + assert_equal 1, r3.errors.count + assert_equal 0, r4.errors.count + r.content = r3.content = "non-empty" + assert t.valid? + end + + def test_validates_associated_one + repair_validations(Reply) do + Reply.validates_associated( :topic ) + Topic.validates_presence_of( :content ) + r = Reply.new("title" => "A reply", "content" => "with content!") + r.topic = Topic.create("title" => "uhohuhoh") + assert !r.valid? + assert r.errors[:topic].any? + r.topic.content = "non-empty" + assert r.valid? + end + end + + def test_validates_associated_with_custom_message_using_quotes + repair_validations(Reply) do + Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" + Topic.validates_presence_of :content + r = Reply.create("title" => "A reply", "content" => "with content!") + r.topic = Topic.create("title" => "uhohuhoh") + assert !r.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], r.errors[:topic] + end + end + + def test_validates_associated_missing + repair_validations(Reply) do + Reply.validates_presence_of(:topic) + r = Reply.create("title" => "A reply", "content" => "with content!") + assert !r.valid? + assert r.errors[:topic].any? + + r.topic = Topic.find :first + assert r.valid? + end + end + + def test_validates_size_of_association_utf8 + repair_validations(Owner) do + with_kcode('UTF8') do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'あいうえおかきくけこ') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'あいうえおかきくけこ') + assert o.valid? + end + end + end +end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb new file mode 100644 index 0000000000..29c10de4fe --- /dev/null +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -0,0 +1,166 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class I18nGenerateMessageValidationTest < Test::Unit::TestCase + def setup + reset_callbacks Topic + @topic = Topic.new + I18n.backend.store_translations :'en', { + :activerecord => { + :errors => { + :messages => { + :taken => "has already been taken", + } + } + } + } + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + def test_generate_message_inclusion_with_default_message + assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') + end + + def test_generate_message_inclusion_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + def test_generate_message_exclusion_with_default_message + assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') + end + + def test_generate_message_exclusion_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + def test_generate_message_invalid_with_default_message + assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') + end + + def test_generate_message_invalid_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) + def test_generate_message_confirmation_with_default_message + assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil) + end + + def test_generate_message_confirmation_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message') + end + + # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) + def test_generate_message_accepted_with_default_message + assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil) + end + + def test_generate_message_accepted_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message') + end + + # add_on_empty: generate_message(attr, :empty, :default => custom_message) + def test_generate_message_empty_with_default_message + assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil) + end + + def test_generate_message_empty_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message') + end + + # add_on_blank: generate_message(attr, :blank, :default => custom_message) + def test_generate_message_blank_with_default_message + assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil) + end + + def test_generate_message_blank_with_custom_message + assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message') + end + + # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + def test_generate_message_too_long_with_default_message + assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10) + end + + def test_generate_message_too_long_with_custom_message + assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + def test_generate_message_too_short_with_default_message + assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10) + end + + def test_generate_message_too_short_with_custom_message + assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) + end + + # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) + def test_generate_message_wrong_length_with_default_message + assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) + end + + def test_generate_message_wrong_length_with_custom_message + assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) + end + + # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + def test_generate_message_not_a_number_with_default_message + assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') + end + + def test_generate_message_not_a_number_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') + end + + # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + def test_generate_message_greater_than_with_default_message + assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_greater_than_or_equal_to_with_default_message + assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_equal_to_with_default_message + assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_with_default_message + assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_less_than_or_equal_to_with_default_message + assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_odd_with_default_message + assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) + end + + def test_generate_message_even_with_default_message + assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) + end + + # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) + def test_generate_message_taken_with_default_message + assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') + end + + def test_generate_message_taken_with_custom_message + assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title') + end + +end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 66982346e9..4b34a65bb7 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'models/topic' require 'models/reply' -class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase +class I18nValidationTest < ActiveRecord::TestCase def setup reset_callbacks Topic @topic = Topic.new @@ -39,40 +39,12 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase end def test_default_error_messages_is_deprecated - assert_deprecated('ActiveRecord::Errors.default_error_messages') do + assert_deprecated('Errors.default_error_messages') do ActiveRecord::Errors.default_error_messages end end - def test_percent_s_interpolation_syntax_in_error_messages_still_works - ActiveSupport::Deprecation.silence do - result = I18n.t :does_not_exist, :default => "%s interpolation syntax is deprecated", :value => 'this' - assert_equal result, "this interpolation syntax is deprecated" - end - end - - def test_percent_s_interpolation_syntax_in_error_messages_is_deprecated - assert_deprecated('using %s in messages') do - I18n.t :does_not_exist, :default => "%s interpolation syntax is deprected", :value => 'this' - end - end - - def test_percent_d_interpolation_syntax_in_error_messages_still_works - ActiveSupport::Deprecation.silence do - result = I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprecated", :count => 2 - assert_equal result, "2 interpolation syntaxes are deprecated" - end - end - - def test_percent_d_interpolation_syntax_in_error_messages_is_deprecated - assert_deprecated('using %d in messages') do - I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprected", :count => 2 - end - end - - # ActiveRecord::Errors def test_errors_generate_message_translates_custom_model_attribute_key - I18n.expects(:translate).with( :topic, { :count => 1, @@ -140,6 +112,55 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase end + # validates_uniqueness_of w/ mocha + + def test_validates_uniqueness_of_generates_message + Topic.validates_uniqueness_of :title + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'}) + @topic.valid? + end + + def test_validates_uniqueness_of_generates_message_with_custom_default_message + Topic.validates_uniqueness_of :title, :message => 'custom' + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'}) + @topic.valid? + end + + # validates_associated w/ mocha + + def test_validates_associated_generates_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + def test_validates_associated_generates_message_with_custom_default_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + # validates_associated w/o mocha + + def test_validates_associated_finds_custom_model_key_translation + I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal ['custom message'], replied_topic.errors[:replies] + end + + def test_validates_associated_finds_global_default_translation + I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal ['global message'], replied_topic.errors[:replies] + end + def test_errors_add_on_empty_generates_message @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) @topic.errors.add_on_empty :title @@ -161,7 +182,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase end def test_errors_full_messages_translates_human_attribute_name_for_model_attributes - @topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] } + @topic.errors.add('title', 'empty') I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title') @topic.errors.full_messages :locale => 'en' end @@ -278,22 +299,6 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase @topic.valid? end - # validates_uniqueness_of w/ mocha - - def test_validates_uniqueness_of_generates_message - Topic.validates_uniqueness_of :title - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'}) - @topic.valid? - end - - def test_validates_uniqueness_of_generates_message_with_custom_default_message - Topic.validates_uniqueness_of :title, :message => 'custom' - @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'}) - @topic.valid? - end - # validates_format_of w/ mocha def test_validates_format_of_generates_message @@ -406,20 +411,6 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase @topic.valid? end - # validates_associated w/ mocha - - def test_validates_associated_generates_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end - - def test_validates_associated_generates_message_with_custom_default_message - Topic.validates_associated :replies - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) - replied_topic.valid? - end - # validates_confirmation_of w/o mocha def test_validates_confirmation_of_finds_custom_model_key_translation @@ -429,7 +420,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_confirmation_of_finds_global_default_translation @@ -438,7 +429,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_confirmation_of :title @topic.title_confirmation = 'foo' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_acceptance_of w/o mocha @@ -449,7 +440,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_acceptance_of_finds_global_default_translation @@ -457,7 +448,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_acceptance_of :title, :allow_nil => false @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_presence_of w/o mocha @@ -468,7 +459,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_presence_of_finds_global_default_translation @@ -476,7 +467,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_length_of :within w/o mocha @@ -487,7 +478,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :within => 3..5 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_length_of_within_finds_global_default_translation @@ -495,7 +486,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :within => 3..5 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_length_of :is w/o mocha @@ -506,7 +497,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_length_of_is_finds_global_default_translation @@ -514,18 +505,16 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end - # validates_uniqueness_of w/o mocha - def test_validates_length_of_is_finds_custom_model_key_translation I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}} Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_length_of_is_finds_global_default_translation @@ -533,7 +522,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_length_of :title, :is => 5 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end @@ -545,7 +534,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_format_of_finds_global_default_translation @@ -553,7 +542,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_inclusion_of w/o mocha @@ -564,7 +553,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_inclusion_of_finds_global_default_translation @@ -572,7 +561,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_inclusion_of :title, :in => %w(a b c) @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_exclusion_of w/o mocha @@ -584,7 +573,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_exclusion_of_finds_global_default_translation @@ -593,7 +582,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_exclusion_of :title, :in => %w(a b c) @topic.title = 'a' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of without :only_integer w/o mocha @@ -605,7 +594,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title @topic.title = 'a' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_finds_global_default_translation @@ -614,7 +603,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of with :only_integer w/o mocha @@ -626,7 +615,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_only_integer_finds_global_default_translation @@ -635,7 +624,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true @topic.title = 'a' @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of :odd w/o mocha @@ -647,7 +636,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_odd_finds_global_default_translation @@ -656,7 +645,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :odd => true @topic.title = 0 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) + assert_equal ['global message'], @topic.errors[:title] end # validates_numericality_of :less_than w/o mocha @@ -668,7 +657,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @topic.valid? - assert_equal 'custom message', @topic.errors.on(:title) + assert_equal ['custom message'], @topic.errors[:title] end def test_validates_numericality_of_less_than_finds_global_default_translation @@ -677,27 +666,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 @topic.title = 1 @topic.valid? - assert_equal 'global message', @topic.errors.on(:title) - end - - - # validates_associated w/o mocha - - def test_validates_associated_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} - - Topic.validates_associated :replies - replied_topic.valid? - assert_equal 'custom message', replied_topic.errors.on(:replies) - end - - def test_validates_associated_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} - - Topic.validates_associated :replies - replied_topic.valid? - assert_equal 'global message', replied_topic.errors.on(:replies) + assert_equal ['global message'], @topic.errors[:title] end def test_validations_with_message_symbol_must_translate @@ -705,7 +674,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title, :message => :custom_error @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) + assert_equal ["I am a custom error"], @topic.errors[:title] end def test_validates_with_message_symbol_must_translate_per_attribute @@ -713,7 +682,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title, :message => :custom_error @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) + assert_equal ["I am a custom error"], @topic.errors[:title] end def test_validates_with_message_symbol_must_translate_per_model @@ -721,195 +690,13 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase Topic.validates_presence_of :title, :message => :custom_error @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) + assert_equal ["I am a custom error"], @topic.errors[:title] end def test_validates_with_message_string Topic.validates_presence_of :title, :message => "I am a custom error" @topic.title = nil @topic.valid? - assert_equal "I am a custom error", @topic.errors.on(:title) - end - -end - -class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase - def setup - reset_callbacks Topic - @topic = Topic.new - I18n.backend.store_translations :'en', { - :activerecord => { - :errors => { - :messages => { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is {{count}} characters)", - :too_short => "is too short (minimum is {{count}} characters)", - :wrong_length => "is the wrong length (should be {{count}} characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than {{count}}", - :greater_than_or_equal_to => "must be greater than or equal to {{count}}", - :equal_to => "must be equal to {{count}}", - :less_than => "must be less than {{count}}", - :less_than_or_equal_to => "must be less than or equal to {{count}}", - :odd => "must be odd", - :even => "must be even" - } - } - } - } - end - - def reset_callbacks(*models) - models.each do |model| - model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) - model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new) - model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new) - end - end - - # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) - def test_generate_message_inclusion_with_default_message - assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') - end - - def test_generate_message_inclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) - def test_generate_message_exclusion_with_default_message - assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') - end - - def test_generate_message_exclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - def test_generate_message_invalid_with_default_message - assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') - end - - def test_generate_message_invalid_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) - def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil) - end - - def test_generate_message_confirmation_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message') + assert_equal ["I am a custom error"], @topic.errors[:title] end - - # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) - def test_generate_message_accepted_with_default_message - assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil) - end - - def test_generate_message_accepted_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message') - end - - # add_on_empty: generate_message(attr, :empty, :default => custom_message) - def test_generate_message_empty_with_default_message - assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil) - end - - def test_generate_message_empty_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message') - end - - # add_on_blank: generate_message(attr, :blank, :default => custom_message) - def test_generate_message_blank_with_default_message - assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil) - end - - def test_generate_message_blank_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message') - end - - # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) - def test_generate_message_too_long_with_default_message - assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10) - end - - def test_generate_message_too_long_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) - def test_generate_message_too_short_with_default_message - assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10) - end - - def test_generate_message_too_short_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) - def test_generate_message_wrong_length_with_default_message - assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) - end - - def test_generate_message_wrong_length_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) - end - - # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) - def test_generate_message_taken_with_default_message - assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') - end - - def test_generate_message_taken_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - def test_generate_message_not_a_number_with_default_message - assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') - end - - def test_generate_message_not_a_number_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) - def test_generate_message_greater_than_with_default_message - assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_greater_than_or_equal_to_with_default_message - assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_equal_to_with_default_message - assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_with_default_message - assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_or_equal_to_with_default_message - assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_odd_with_default_message - assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_even_with_default_message - assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) - end - end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb new file mode 100644 index 0000000000..961db51d1d --- /dev/null +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -0,0 +1,278 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/topic' +require 'models/reply' +require 'models/warehouse_thing' +require 'models/guid' +require 'models/event' + +# The following methods in Topic are used in test_conditional_validation_* +class Topic + has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" + has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" +end + +class UniqueReply < Reply + validates_uniqueness_of :content, :scope => 'parent_id' +end + +class SillyUniqueReply < UniqueReply +end + +class Wizard < ActiveRecord::Base + self.abstract_class = true + + validates_uniqueness_of :name +end + +class IneptWizard < Wizard + validates_uniqueness_of :city +end + +class Conjurer < IneptWizard +end + +class Thaumaturgist < IneptWizard +end + +class UniquenessValidationTest < ActiveRecord::TestCase + fixtures :topics, 'warehouse-things' + + repair_validations(Topic) + + def test_validate_uniqueness + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'm unique!") + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert_equal ["has already been taken"], t2.errors[:title] + + t2.title = "Now Im really also unique" + assert t2.save, "Should now save t2 as unique" + end + + def test_validates_uniquness_with_newline_chars + Topic.validates_uniqueness_of(:title, :case_sensitive => false) + + t = Topic.new("title" => "new\nline") + assert t.save, "Should save t as unique" + end + + def test_validate_uniqueness_with_scope + repair_validations(Reply) do + Reply.validates_uniqueness_of(:content, :scope => "parent_id") + + t = Topic.create("title" => "I'm unique!") + + r1 = t.replies.create "title" => "r1", "content" => "hello world" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "title" => "r2", "content" => "hello world" + assert !r2.valid?, "Saving r2 first time" + + r2.content = "something else" + assert r2.save, "Saving r2 second time" + + t2 = Topic.create("title" => "I'm unique too!") + r3 = t2.replies.create "title" => "r3", "content" => "hello world" + assert r3.valid?, "Saving r3" + end + end + + def test_validate_uniqueness_scoped_to_defining_class + t = Topic.create("title" => "What, me worry?") + + r1 = t.unique_replies.create "title" => "r1", "content" => "a barrel of fun" + assert r1.valid?, "Saving r1" + + r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun" + assert !r2.valid?, "Saving r2" + + # Should succeed as validates_uniqueness_of only applies to + # UniqueReply and its subclasses + r3 = t.replies.create "title" => "r2", "content" => "a barrel of fun" + assert r3.valid?, "Saving r3" + end + + def test_validate_uniqueness_with_scope_array + repair_validations(Reply) do + Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id]) + + t = Topic.create("title" => "The earth is actually flat!") + + r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..." + assert !r2.valid?, "Saving r2. Double reply by same author." + + r2.author_email_address = "jeremy_alt_email@rubyonrails.com" + assert r2.save, "Saving r2 the second time." + + r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic" + assert !r3.valid?, "Saving r3" + + r3.author_name = "jj" + assert r3.save, "Saving r3 the second time." + + r3.author_name = "jeremy" + assert !r3.save, "Saving r3 the third time." + end + end + + def test_validate_case_insensitive_uniqueness + Topic.validates_uniqueness_of(:title, :parent_id, :case_sensitive => false, :allow_nil => true) + + t = Topic.new("title" => "I'm unique!", :parent_id => 2) + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1) + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert t2.errors[:title].any? + assert t2.errors[:parent_id].any? + assert_equal ["has already been taken"], t2.errors[:title] + + t2.title = "I'm truly UNIQUE!" + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert t2.errors[:title].empty? + assert t2.errors[:parent_id].any? + + t2.parent_id = 4 + assert t2.save, "Should now save t2 as unique" + + t2.parent_id = nil + t2.title = nil + assert t2.valid?, "should validate with nil" + assert t2.save, "should save with nil" + + with_kcode('UTF8') do + t_utf8 = Topic.new("title" => "Я тоже уникальный!") + assert t_utf8.save, "Should save t_utf8 as unique" + + # If database hasn't UTF-8 character set, this test fails + if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" + t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") + assert !t2_utf8.valid?, "Shouldn't be valid" + assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" + end + end + end + + def test_validate_case_sensitive_uniqueness + Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'M UNIQUE!") + assert t2.valid?, "Should be valid" + assert t2.save, "Should save t2 as unique" + assert t2.errors[:title].empty? + assert t2.errors[:parent_id].empty? + assert_not_equal ["has already been taken"], t2.errors[:title] + + t3 = Topic.new("title" => "I'M uNiQUe!") + assert t3.valid?, "Should be valid" + assert t3.save, "Should save t2 as unique" + assert t3.errors[:title].empty? + assert t3.errors[:parent_id].empty? + assert_not_equal ["has already been taken"], t3.errors[:title] + end + + def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer + Topic.validates_uniqueness_of(:title, :case_sensitve => true) + t = Topic.create!('title' => 101) + + t2 = Topic.new('title' => 101) + assert !t2.valid? + assert t2.errors[:title] + end + + def test_validate_uniqueness_with_non_standard_table_names + i1 = WarehouseThing.create(:value => 1000) + assert !i1.valid?, "i1 should not be valid" + assert i1.errors[:value].any?, "Should not be empty" + end + + def test_validates_uniqueness_inside_with_scope + Topic.validates_uniqueness_of(:title) + + Topic.with_scope(:find => { :conditions => { :author_name => "David" } }) do + t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") + assert t1.save + t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") + assert !t2.valid? + end + end + + def test_validate_uniqueness_with_columns_which_are_sql_keywords + repair_validations(Guid) do + Guid.validates_uniqueness_of :key + g = Guid.new + g.key = "foo" + assert_nothing_raised { !g.valid? } + end + end + + def test_validate_uniqueness_with_limit + # Event.title is limited to 5 characters + e1 = Event.create(:title => "abcde") + assert e1.valid?, "Could not create an event with a unique, 5 character title" + e2 = Event.create(:title => "abcdefgh") + assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" + end + + def test_validate_straight_inheritance_uniqueness + w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork") + assert w1.valid?, "Saving w1" + + # Should use validation from base class (which is abstract) + w2 = IneptWizard.new(:name => "Rincewind", :city => "Quirm") + assert !w2.valid?, "w2 shouldn't be valid" + assert w2.errors[:name].any?, "Should have errors for name" + assert_equal ["has already been taken"], w2.errors[:name], "Should have uniqueness message for name" + + w3 = Conjurer.new(:name => "Rincewind", :city => "Quirm") + assert !w3.valid?, "w3 shouldn't be valid" + assert w3.errors[:name].any?, "Should have errors for name" + assert_equal ["has already been taken"], w3.errors[:name], "Should have uniqueness message for name" + + w4 = Conjurer.create(:name => "The Amazing Bonko", :city => "Quirm") + assert w4.valid?, "Saving w4" + + w5 = Thaumaturgist.new(:name => "The Amazing Bonko", :city => "Lancre") + assert !w5.valid?, "w5 shouldn't be valid" + assert w5.errors[:name].any?, "Should have errors for name" + assert_equal ["has already been taken"], w5.errors[:name], "Should have uniqueness message for name" + + w6 = Thaumaturgist.new(:name => "Mustrum Ridcully", :city => "Quirm") + assert !w6.valid?, "w6 shouldn't be valid" + assert w6.errors[:city].any?, "Should have errors for city" + assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city" + end + + def test_validates_uniqueness_of_with_custom_message_using_quotes + repair_validations(Developer) do + Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes" + d = Developer.new + d.name = "David" + assert !d.valid? + assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name] + end + end +end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index c20f5ae63e..a4e874e5e6 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -10,94 +10,43 @@ require 'models/owner' require 'models/pet' require 'models/event' -# The following methods in Topic are used in test_conditional_validation_* -class Topic - has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" - has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" - - def condition_is_true - true - end - - def condition_is_true_but_its_not - false - end -end - class ProtectedPerson < ActiveRecord::Base set_table_name 'people' attr_accessor :addon attr_protected :first_name end -class UniqueReply < Reply - validates_uniqueness_of :content, :scope => 'parent_id' -end - -class SillyUniqueReply < UniqueReply -end - -class Wizard < ActiveRecord::Base - self.abstract_class = true +class DeprecatedPerson < ActiveRecord::Base + set_table_name 'people' - validates_uniqueness_of :name -end + protected -class IneptWizard < Wizard - validates_uniqueness_of :city -end + def validate + errors[:name] << "always invalid" + end -class Conjurer < IneptWizard -end + def validate_on_create + errors[:name] << "invalid on create" + end -class Thaumaturgist < IneptWizard + def validate_on_update + errors[:name] << "invalid on update" + end end - class ValidationsTest < ActiveRecord::TestCase - fixtures :topics, :developers, 'warehouse-things' + fixtures :topics, :developers # Most of the tests mess with the validations of Topic, so lets repair it all the time. # Other classes we mess with will be dealt with in the specific tests repair_validations(Topic) - def test_single_field_validation - r = Reply.new - r.title = "There's no content!" - assert !r.valid?, "A reply without content shouldn't be saveable" - - r.content = "Messa content!" - assert r.valid?, "A reply with content should be saveable" - end - - def test_single_attr_validation_and_error_msg - r = Reply.new - r.title = "There's no content!" - assert !r.valid? - assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid" - assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error" - assert_equal 1, r.errors.count - end - - def test_double_attr_validation_and_error_msg - r = Reply.new - assert !r.valid? - - assert r.errors.invalid?("title"), "A reply without title should mark that attribute as invalid" - assert_equal "Empty", r.errors.on("title"), "A reply without title should contain an error" - - assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid" - assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error" - - assert_equal 2, r.errors.count - end - def test_error_on_create r = Reply.new r.title = "Wrong Create" assert !r.valid? - assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid" - assert_equal "is Wrong Create", r.errors.on("title"), "A reply with a bad content should contain an error" + assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" + assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error" end def test_error_on_update @@ -109,8 +58,8 @@ class ValidationsTest < ActiveRecord::TestCase r.title = "Wrong Update" assert !r.save, "Second save should fail" - assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid" - assert_equal "is Wrong Update", r.errors.on("title"), "A reply with a bad content should contain an error" + assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" + assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error" end def test_invalid_record_exception @@ -172,47 +121,6 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_single_error_per_attr_iteration - r = Reply.new - r.save - - errors = [] - r.errors.each { |attr, msg| errors << [attr, msg] } - - assert errors.include?(["title", "Empty"]) - assert errors.include?(["content", "Empty"]) - end - - def test_multiple_errors_per_attr_iteration_with_full_error_composition - r = Reply.new - r.title = "Wrong Create" - r.content = "Mismatch" - r.save - - errors = [] - r.errors.each_full { |error| errors << error } - - assert_equal "Title is Wrong Create", errors[0] - assert_equal "Title is Content Mismatch", errors[1] - assert_equal 2, r.errors.count - end - - def test_errors_on_base - r = Reply.new - r.content = "Mismatch" - r.save - r.errors.add_to_base "Reply is not dignifying" - - errors = [] - r.errors.each_full { |error| errors << error } - - assert_equal "Reply is not dignifying", r.errors.on_base - - assert errors.include?("Title Empty") - assert errors.include?("Reply is not dignifying") - assert_equal 2, r.errors.count - end - def test_create_without_validation reply = Reply.new assert !reply.save @@ -225,96 +133,6 @@ class ValidationsTest < ActiveRecord::TestCase assert count+1, Reply.count end - def test_validates_each - hits = 0 - Topic.validates_each(:title, :content, [:title, :content]) do |record, attr| - record.errors.add attr, 'gotcha' - hits += 1 - end - t = Topic.new("title" => "valid", "content" => "whatever") - assert !t.save - assert_equal 4, hits - assert_equal %w(gotcha gotcha), t.errors.on(:title) - assert_equal %w(gotcha gotcha), t.errors.on(:content) - end - - def test_no_title_confirmation - Topic.validates_confirmation_of(:title) - - t = Topic.new(:author_name => "Plutarch") - assert t.valid? - - t.title_confirmation = "Parallel Lives" - assert !t.valid? - - t.title_confirmation = nil - t.title = "Parallel Lives" - assert t.valid? - - t.title_confirmation = "Parallel Lives" - assert t.valid? - end - - def test_title_confirmation - Topic.validates_confirmation_of(:title) - - t = Topic.create("title" => "We should be confirmed","title_confirmation" => "") - assert !t.save - - t.title_confirmation = "We should be confirmed" - assert t.save - end - - def test_terms_of_service_agreement_no_acceptance - Topic.validates_acceptance_of(:terms_of_service, :on => :create) - - t = Topic.create("title" => "We should not be confirmed") - assert t.save - end - - def test_terms_of_service_agreement - Topic.validates_acceptance_of(:terms_of_service, :on => :create) - - t = Topic.create("title" => "We should be confirmed","terms_of_service" => "") - assert !t.save - assert_equal "must be accepted", t.errors.on(:terms_of_service) - - t.terms_of_service = "1" - assert t.save - end - - - def test_eula - Topic.validates_acceptance_of(:eula, :message => "must be abided", :on => :create) - - t = Topic.create("title" => "We should be confirmed","eula" => "") - assert !t.save - assert_equal "must be abided", t.errors.on(:eula) - - t.eula = "1" - assert t.save - end - - def test_terms_of_service_agreement_with_accept_value - Topic.validates_acceptance_of(:terms_of_service, :on => :create, :accept => "I agree.") - - t = Topic.create("title" => "We should be confirmed", "terms_of_service" => "") - assert !t.save - assert_equal "must be accepted", t.errors.on(:terms_of_service) - - t.terms_of_service = "I agree." - assert t.save - end - - def test_validates_acceptance_of_as_database_column - repair_validations(Reply) do - Reply.validates_acceptance_of(:author_name) - - reply = Reply.create("author_name" => "Dan Brown") - assert_equal "Dan Brown", reply["author_name"] - end - end - def test_validates_acceptance_of_with_non_existant_table Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base) @@ -323,868 +141,6 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_validate_presences - Topic.validates_presence_of(:title, :content) - - t = Topic.create - assert !t.save - assert_equal "can't be blank", t.errors.on(:title) - assert_equal "can't be blank", t.errors.on(:content) - - t.title = "something" - t.content = " " - - assert !t.save - assert_equal "can't be blank", t.errors.on(:content) - - t.content = "like stuff" - - assert t.save - end - - def test_validate_uniqueness - Topic.validates_uniqueness_of(:title) - - t = Topic.new("title" => "I'm unique!") - assert t.save, "Should save t as unique" - - t.content = "Remaining unique" - assert t.save, "Should still save t as unique" - - t2 = Topic.new("title" => "I'm unique!") - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert_equal "has already been taken", t2.errors.on(:title) - - t2.title = "Now Im really also unique" - assert t2.save, "Should now save t2 as unique" - end - - def test_validates_uniquness_with_newline_chars - Topic.validates_uniqueness_of(:title, :case_sensitive => false) - - t = Topic.new("title" => "new\nline") - assert t.save, "Should save t as unique" - end - - def test_validate_uniqueness_with_scope - repair_validations(Reply) do - Reply.validates_uniqueness_of(:content, :scope => "parent_id") - - t = Topic.create("title" => "I'm unique!") - - r1 = t.replies.create "title" => "r1", "content" => "hello world" - assert r1.valid?, "Saving r1" - - r2 = t.replies.create "title" => "r2", "content" => "hello world" - assert !r2.valid?, "Saving r2 first time" - - r2.content = "something else" - assert r2.save, "Saving r2 second time" - - t2 = Topic.create("title" => "I'm unique too!") - r3 = t2.replies.create "title" => "r3", "content" => "hello world" - assert r3.valid?, "Saving r3" - end - end - - def test_validate_uniqueness_scoped_to_defining_class - t = Topic.create("title" => "What, me worry?") - - r1 = t.unique_replies.create "title" => "r1", "content" => "a barrel of fun" - assert r1.valid?, "Saving r1" - - r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun" - assert !r2.valid?, "Saving r2" - - # Should succeed as validates_uniqueness_of only applies to - # UniqueReply and its subclasses - r3 = t.replies.create "title" => "r2", "content" => "a barrel of fun" - assert r3.valid?, "Saving r3" - end - - def test_validate_uniqueness_with_scope_array - repair_validations(Reply) do - Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id]) - - t = Topic.create("title" => "The earth is actually flat!") - - r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply" - assert r1.valid?, "Saving r1" - - r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..." - assert !r2.valid?, "Saving r2. Double reply by same author." - - r2.author_email_address = "jeremy_alt_email@rubyonrails.com" - assert r2.save, "Saving r2 the second time." - - r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic" - assert !r3.valid?, "Saving r3" - - r3.author_name = "jj" - assert r3.save, "Saving r3 the second time." - - r3.author_name = "jeremy" - assert !r3.save, "Saving r3 the third time." - end - end - - def test_validate_case_insensitive_uniqueness - Topic.validates_uniqueness_of(:title, :parent_id, :case_sensitive => false, :allow_nil => true) - - t = Topic.new("title" => "I'm unique!", :parent_id => 2) - assert t.save, "Should save t as unique" - - t.content = "Remaining unique" - assert t.save, "Should still save t as unique" - - t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1) - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert t2.errors.on(:title) - assert t2.errors.on(:parent_id) - assert_equal "has already been taken", t2.errors.on(:title) - - t2.title = "I'm truly UNIQUE!" - assert !t2.valid?, "Shouldn't be valid" - assert !t2.save, "Shouldn't save t2 as unique" - assert_nil t2.errors.on(:title) - assert t2.errors.on(:parent_id) - - t2.parent_id = 4 - assert t2.save, "Should now save t2 as unique" - - t2.parent_id = nil - t2.title = nil - assert t2.valid?, "should validate with nil" - assert t2.save, "should save with nil" - - with_kcode('UTF8') do - t_utf8 = Topic.new("title" => "Я тоже уникальный!") - assert t_utf8.save, "Should save t_utf8 as unique" - - # If database hasn't UTF-8 character set, this test fails - if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" - t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") - assert !t2_utf8.valid?, "Shouldn't be valid" - assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" - end - end - end - - def test_validate_case_sensitive_uniqueness - Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true) - - t = Topic.new("title" => "I'm unique!") - assert t.save, "Should save t as unique" - - t.content = "Remaining unique" - assert t.save, "Should still save t as unique" - - t2 = Topic.new("title" => "I'M UNIQUE!") - assert t2.valid?, "Should be valid" - assert t2.save, "Should save t2 as unique" - assert !t2.errors.on(:title) - assert !t2.errors.on(:parent_id) - assert_not_equal "has already been taken", t2.errors.on(:title) - - t3 = Topic.new("title" => "I'M uNiQUe!") - assert t3.valid?, "Should be valid" - assert t3.save, "Should save t2 as unique" - assert !t3.errors.on(:title) - assert !t3.errors.on(:parent_id) - assert_not_equal "has already been taken", t3.errors.on(:title) - end - - def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer - Topic.validates_uniqueness_of(:title, :case_sensitve => true) - t = Topic.create!('title' => 101) - - t2 = Topic.new('title' => 101) - assert !t2.valid? - assert t2.errors.on(:title) - end - - def test_validate_uniqueness_with_non_standard_table_names - i1 = WarehouseThing.create(:value => 1000) - assert !i1.valid?, "i1 should not be valid" - assert i1.errors.on(:value), "Should not be empty" - end - - def test_validates_uniqueness_inside_with_scope - Topic.validates_uniqueness_of(:title) - - Topic.with_scope(:find => { :conditions => { :author_name => "David" } }) do - t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") - assert t1.save - t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") - assert !t2.valid? - end - end - - def test_validate_uniqueness_with_columns_which_are_sql_keywords - repair_validations(Guid) do - Guid.validates_uniqueness_of :key - g = Guid.new - g.key = "foo" - assert_nothing_raised { !g.valid? } - end - end - - def test_validate_uniqueness_with_limit - # Event.title is limited to 5 characters - e1 = Event.create(:title => "abcde") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "abcdefgh") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" - end - - def test_validate_straight_inheritance_uniqueness - w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork") - assert w1.valid?, "Saving w1" - - # Should use validation from base class (which is abstract) - w2 = IneptWizard.new(:name => "Rincewind", :city => "Quirm") - assert !w2.valid?, "w2 shouldn't be valid" - assert w2.errors.on(:name), "Should have errors for name" - assert_equal "has already been taken", w2.errors.on(:name), "Should have uniqueness message for name" - - w3 = Conjurer.new(:name => "Rincewind", :city => "Quirm") - assert !w3.valid?, "w3 shouldn't be valid" - assert w3.errors.on(:name), "Should have errors for name" - assert_equal "has already been taken", w3.errors.on(:name), "Should have uniqueness message for name" - - w4 = Conjurer.create(:name => "The Amazing Bonko", :city => "Quirm") - assert w4.valid?, "Saving w4" - - w5 = Thaumaturgist.new(:name => "The Amazing Bonko", :city => "Lancre") - assert !w5.valid?, "w5 shouldn't be valid" - assert w5.errors.on(:name), "Should have errors for name" - assert_equal "has already been taken", w5.errors.on(:name), "Should have uniqueness message for name" - - w6 = Thaumaturgist.new(:name => "Mustrum Ridcully", :city => "Quirm") - assert !w6.valid?, "w6 shouldn't be valid" - assert w6.errors.on(:city), "Should have errors for city" - assert_equal "has already been taken", w6.errors.on(:city), "Should have uniqueness message for city" - end - - def test_validate_format - Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") - - t = Topic.create("title" => "i'm incorrect", "content" => "Validation macros rule!") - assert !t.valid?, "Shouldn't be valid" - assert !t.save, "Shouldn't save because it's invalid" - assert_equal "is bad data", t.errors.on(:title) - assert_nil t.errors.on(:content) - - t.title = "Validation macros rule!" - - assert t.save - assert_nil t.errors.on(:title) - - assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } - end - - def test_validate_format_with_allow_blank - Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank=>true) - assert !Topic.create("title" => "Shouldn't be valid").valid? - assert Topic.create("title" => "").valid? - assert Topic.create("title" => nil).valid? - assert Topic.create("title" => "Validation macros rule!").valid? - end - - # testing ticket #3142 - def test_validate_format_numeric - Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") - - t = Topic.create("title" => "72x", "content" => "6789") - assert !t.valid?, "Shouldn't be valid" - assert !t.save, "Shouldn't save because it's invalid" - assert_equal "is bad data", t.errors.on(:title) - assert_nil t.errors.on(:content) - - t.title = "-11" - assert !t.valid?, "Shouldn't be valid" - - t.title = "03" - assert !t.valid?, "Shouldn't be valid" - - t.title = "z44" - assert !t.valid?, "Shouldn't be valid" - - t.title = "5v7" - assert !t.valid?, "Shouldn't be valid" - - t.title = "1" - - assert t.save - assert_nil t.errors.on(:title) - end - - def test_validate_format_with_formatted_message - Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be {{value}}") - t = Topic.create(:title => 'Invalid title') - assert_equal "can't be Invalid title", t.errors.on(:title) - end - - def test_validates_inclusion_of - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) ) - - assert !Topic.create("title" => "a!", "content" => "abc").valid? - assert !Topic.create("title" => "a b", "content" => "abc").valid? - assert !Topic.create("title" => nil, "content" => "def").valid? - - t = Topic.create("title" => "a", "content" => "I know you are but what am I?") - assert t.valid? - t.title = "uhoh" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is not included in the list", t.errors["title"] - - assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) } - assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) } - - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) } - assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) } - end - - def test_validates_inclusion_of_with_allow_nil - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true ) - - assert !Topic.create("title" => "a!", "content" => "abc").valid? - assert !Topic.create("title" => "", "content" => "abc").valid? - assert Topic.create("title" => nil, "content" => "abc").valid? - end - - def test_numericality_with_getter_method - repair_validations(Developer) do - Developer.validates_numericality_of( :salary ) - developer = Developer.new("name" => "michael", "salary" => nil) - developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") - assert developer.valid? - end - end - - def test_validates_length_of_with_allow_nil - Topic.validates_length_of( :title, :is => 5, :allow_nil=>true ) - - assert !Topic.create("title" => "ab").valid? - assert !Topic.create("title" => "").valid? - assert Topic.create("title" => nil).valid? - assert Topic.create("title" => "abcde").valid? - end - - def test_validates_length_of_with_allow_blank - Topic.validates_length_of( :title, :is => 5, :allow_blank=>true ) - - assert !Topic.create("title" => "ab").valid? - assert Topic.create("title" => "").valid? - assert Topic.create("title" => nil).valid? - assert Topic.create("title" => "abcde").valid? - end - - def test_validates_inclusion_of_with_formatted_message - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option {{value}} is not in the list" ) - - assert Topic.create("title" => "a", "content" => "abc").valid? - - t = Topic.create("title" => "uhoh", "content" => "abc") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "option uhoh is not in the list", t.errors["title"] - end - - def test_numericality_with_allow_nil_and_getter_method - repair_validations(Developer) do - Developer.validates_numericality_of( :salary, :allow_nil => true) - developer = Developer.new("name" => "michael", "salary" => nil) - developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end") - assert developer.valid? - end - end - - def test_validates_exclusion_of - Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) ) - - assert Topic.create("title" => "something", "content" => "abc").valid? - assert !Topic.create("title" => "monkey", "content" => "abc").valid? - end - - def test_validates_exclusion_of_with_formatted_message - Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option {{value}} is restricted" ) - - assert Topic.create("title" => "something", "content" => "abc") - - t = Topic.create("title" => "monkey") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "option monkey is restricted", t.errors["title"] - end - - def test_validates_length_of_using_minimum - Topic.validates_length_of :title, :minimum => 5 - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = "not" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - - t.title = "" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - - t.title = nil - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - end - - def test_optionally_validates_length_of_using_minimum - Topic.validates_length_of :title, :minimum => 5, :allow_nil => true - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_validates_length_of_using_maximum - Topic.validates_length_of :title, :maximum => 5 - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = "notvalid" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors["title"] - - t.title = "" - assert t.valid? - - t.title = nil - assert !t.valid? - end - - def test_optionally_validates_length_of_using_maximum - Topic.validates_length_of :title, :maximum => 5, :allow_nil => true - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_validates_length_of_using_within - Topic.validates_length_of(:title, :content, :within => 3..5) - - t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") - assert !t.valid? - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content) - - t.title = nil - t.content = nil - assert !t.valid? - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:content) - - t.title = "abe" - t.content = "mad" - assert t.valid? - end - - def test_optionally_validates_length_of_using_within - Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true - - t = Topic.create('title' => 'abc', 'content' => 'abcd') - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_optionally_validates_length_of_using_within_on_create - Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: {{count}}" - - t = Topic.create("title" => "thisisnotvalid", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - assert_equal "my string is too long: 10", t.errors[:title] - - t.title = "butthisis" - assert t.save - - t.title = "few" - assert t.save - - t.content = "andthisislong" - assert t.save - - t.content = t.title = "iamfine" - assert t.save - end - - def test_optionally_validates_length_of_using_within_on_update - Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: {{count}}" - - t = Topic.create("title" => "vali", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - - t.title = "not" - assert !t.save - assert t.errors.on(:title) - assert_equal "my string is too short: 5", t.errors[:title] - - t.title = "valid" - t.content = "andthisistoolong" - assert !t.save - assert t.errors.on(:content) - - t.content = "iamfine" - assert t.save - end - - def test_validates_length_of_using_is - Topic.validates_length_of :title, :is => 5 - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = "notvalid" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is the wrong length (should be 5 characters)", t.errors["title"] - - t.title = "" - assert !t.valid? - - t.title = nil - assert !t.valid? - end - - def test_optionally_validates_length_of_using_is - Topic.validates_length_of :title, :is => 5, :allow_nil => true - - t = Topic.create("title" => "valid", "content" => "whatever") - assert t.valid? - - t.title = nil - assert t.valid? - end - - def test_validates_length_of_using_bignum - bigmin = 2 ** 30 - bigmax = 2 ** 32 - bigrange = bigmin...bigmax - assert_nothing_raised do - Topic.validates_length_of :title, :is => bigmin + 5 - Topic.validates_length_of :title, :within => bigrange - Topic.validates_length_of :title, :in => bigrange - Topic.validates_length_of :title, :minimum => bigmin - Topic.validates_length_of :title, :maximum => bigmax - end - end - - def test_validates_length_with_globally_modified_error_message - ActiveSupport::Deprecation.silence do - ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}' - end - Topic.validates_length_of :title, :minimum => 10 - t = Topic.create(:title => 'too short') - assert !t.valid? - - assert_equal 'tu est trops petit hombre 10', t.errors['title'] - end - - def test_validates_size_of_association - repair_validations(Owner) do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors.on(:pets) - pet = o.pets.build('name' => 'apet') - assert o.valid? - end - end - - def test_validates_size_of_association_using_within - repair_validations(Owner) do - assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors.on(:pets) - - pet = o.pets.build('name' => 'apet') - assert o.valid? - - 2.times { o.pets.build('name' => 'apet') } - assert !o.save - assert o.errors.on(:pets) - end - end - - def test_validates_length_of_nasty_params - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>6, :maximum=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :maximum=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :minimum=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6, :is=>9) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") } - end - - def test_validates_length_of_custom_errors_for_minimum_with_message - Topic.validates_length_of( :title, :minimum=>5, :message=>"boo {{count}}" ) - t = Topic.create("title" => "uhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "boo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_minimum_with_too_short - Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo {{count}}" ) - t = Topic.create("title" => "uhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_maximum_with_message - Topic.validates_length_of( :title, :maximum=>5, :message=>"boo {{count}}" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "boo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_in - Topic.validates_length_of(:title, :in => 10..20, :message => "hoo {{count}}") - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 10", t.errors["title"] - - t = Topic.create("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 20", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_maximum_with_too_long - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" ) - 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_validates_length_of_custom_errors_for_is_with_message - Topic.validates_length_of( :title, :is=>5, :message=>"boo {{count}}" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "boo 5", t.errors["title"] - end - - def test_validates_length_of_custom_errors_for_is_with_wrong_length - Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo {{count}}" ) - 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_validates_length_of_using_minimum_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :minimum => 5 - - t = Topic.create("title" => "一二三四五", "content" => "whatever") - assert t.valid? - - t.title = "一二三四" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too short (minimum is 5 characters)", t.errors["title"] - end - end - - def test_validates_length_of_using_maximum_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :maximum => 5 - - t = Topic.create("title" => "一二三四五", "content" => "whatever") - assert t.valid? - - t.title = "一二34五六" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors["title"] - end - end - - def test_validates_length_of_using_within_utf8 - with_kcode('UTF8') do - Topic.validates_length_of(:title, :content, :within => 3..5) - - t = Topic.new("title" => "一二", "content" => "12三四五六七") - assert !t.valid? - assert_equal "is too short (minimum is 3 characters)", t.errors.on(:title) - assert_equal "is too long (maximum is 5 characters)", t.errors.on(:content) - t.title = "一二三" - t.content = "12三" - assert t.valid? - end - end - - def test_optionally_validates_length_of_using_within_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 3..5, :allow_nil => true - - t = Topic.create(:title => "一二三四五") - assert t.valid?, t.errors.inspect - - t = Topic.create(:title => "一二三") - assert t.valid?, t.errors.inspect - - t.title = nil - assert t.valid?, t.errors.inspect - end - end - - def test_optionally_validates_length_of_using_within_on_create_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: {{count}}" - - t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - assert_equal "長すぎます: 10", t.errors[:title] - - t.title = "一二三四五六七八九" - assert t.save - - t.title = "一二3" - assert t.save - - t.content = "一二三四五六七八九十" - assert t.save - - t.content = t.title = "一二三四五六" - assert t.save - end - end - - def test_optionally_validates_length_of_using_within_on_update_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: {{count}}" - - t = Topic.create("title" => "一二三4", "content" => "whatever") - assert !t.save - assert t.errors.on(:title) - - t.title = "1二三4" - assert !t.save - assert t.errors.on(:title) - assert_equal "短すぎます: 5", t.errors[:title] - - t.title = "一二三四五六七八九十A" - assert !t.save - assert t.errors.on(:title) - - t.title = "一二345" - assert t.save - end - end - - def test_validates_length_of_using_is_utf8 - with_kcode('UTF8') do - Topic.validates_length_of :title, :is => 5 - - t = Topic.create("title" => "一二345", "content" => "whatever") - assert t.valid? - - t.title = "一二345六" - assert !t.valid? - assert t.errors.on(:title) - assert_equal "is the wrong length (should be 5 characters)", t.errors["title"] - end - end - - def test_validates_length_of_with_block - Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least {{count}} words.", - :tokenizer => lambda {|str| str.scan(/\w+/) } - t = Topic.create!(:content => "this content should be long enough") - assert t.valid? - - t.content = "not long enough" - assert !t.valid? - assert t.errors.on(:content) - assert_equal "Your essay must be at least 5 words.", t.errors[:content] - end - - def test_validates_size_of_association_utf8 - repair_validations(Owner) do - with_kcode('UTF8') do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'あいうえおかきくけこ') - assert !o.save - assert o.errors.on(:pets) - o.pets.build('name' => 'あいうえおかきくけこ') - assert o.valid? - end - end - end - - def test_validates_associated_many - Topic.validates_associated( :replies ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")] - assert !t.valid? - assert t.errors.on(:replies) - assert_equal 1, r.errors.count # make sure all associated objects have been validated - assert_equal 0, r2.errors.count - assert_equal 1, r3.errors.count - assert_equal 0, r4.errors.count - r.content = r3.content = "non-empty" - assert t.valid? - end - - def test_validates_associated_one - repair_validations(Reply) do - Reply.validates_associated( :topic ) - Topic.validates_presence_of( :content ) - r = Reply.new("title" => "A reply", "content" => "with content!") - r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? - assert r.errors.on(:topic) - r.topic.content = "non-empty" - assert r.valid? - end - end - - def test_validate_block - Topic.validate { |topic| topic.errors.add("title", "will never be valid") } - t = Topic.create("title" => "Title", "content" => "whatever") - assert !t.valid? - assert t.errors.on(:title) - assert_equal "will never be valid", t.errors["title"] - end - - def test_invalid_validator - Topic.validate 3 - assert_raise(ArgumentError) { t = Topic.create } - end - def test_throw_away_typing d = Developer.new("name" => "David", "salary" => "100,000") assert !d.valid? @@ -1192,411 +148,40 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal "100,000", d.salary_before_type_cast end - def test_validates_acceptance_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.salary = "0" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last - end - end - - def test_validates_confirmation_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes" - d = Developer.new - d.name = "John" - d.name_confirmation = "Johnny" - assert !d.valid? - assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_format_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes" - d = Developer.new - d.name = d.name_confirmation = "John 32" - assert !d.valid? - assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_inclusion_of_with_custom_error_using_quotes - repair_validations(Developer) do - Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.salary = "90,000" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last - end - end - - def test_validates_length_of_with_custom_too_long_using_quotes - repair_validations(Developer) do - Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Jeffrey" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_length_of_with_custom_too_short_using_quotes - repair_validations(Developer) do - Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Joe" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_length_of_with_custom_message_using_quotes - repair_validations(Developer) do - Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Joe" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_presence_of_with_custom_message_using_quotes - repair_validations(Developer) do - Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "Joe" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent) - end - end - - def test_validates_uniqueness_of_with_custom_message_using_quotes - repair_validations(Developer) do - Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes" - d = Developer.new - d.name = "David" - assert !d.valid? - assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name) - end - end - - def test_validates_associated_with_custom_message_using_quotes - repair_validations(Reply) do - Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" - Topic.validates_presence_of :content - r = Reply.create("title" => "A reply", "content" => "with content!") - r.topic = Topic.create("title" => "uhohuhoh") - assert !r.valid? - assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic) + def test_validates_length_with_globally_modified_error_message + ActiveSupport::Deprecation.silence do + ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}' end - end - def test_if_validation_using_method_true - # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :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_unless_validation_using_method_true - # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_if_validation_using_method_false - # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :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_unless_validation_using_method_false - # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true_but_its_not ) - 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_if_validation_using_string_true - # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :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_unless_validation_using_string_true - # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "a = 1; a == 1" ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_if_validation_using_string_false - # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "false") - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - - def test_unless_validation_using_string_false - # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "false") - 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_if_validation_using_block_true - # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :if => Proc.new { |r| r.content.size > 4 } ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") + Topic.validates_length_of :title, :minimum => 10 + t = Topic.create(:title => 'too short') assert !t.valid? - assert t.errors.on(:title) - assert_equal "hoo 5", t.errors["title"] - end - - def test_unless_validation_using_block_true - # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :unless => Proc.new { |r| r.content.size > 4 } ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) - end - def test_if_validation_using_block_false - # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :if => Proc.new { |r| r.title != "uhohuhoh"} ) - t = Topic.create("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? - assert !t.errors.on(:title) + assert_equal ['tu est trops petit hombre 10'], t.errors[:title] end - def test_unless_validation_using_block_false - # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", - :unless => Proc.new { |r| r.title != "uhohuhoh"} ) - 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_validates_associated_missing + def test_validates_acceptance_of_as_database_column repair_validations(Reply) do - Reply.validates_presence_of(:topic) - r = Reply.create("title" => "A reply", "content" => "with content!") - assert !r.valid? - assert r.errors.on(:topic) + Reply.validates_acceptance_of(:author_name) - r.topic = Topic.find :first - assert r.valid? + reply = Reply.create("author_name" => "Dan Brown") + assert_equal "Dan Brown", reply["author_name"] end end - def test_errors_to_xml - r = Reply.new :title => "Wrong Create" - assert !r.valid? - xml = r.errors.to_xml(:skip_instruct => true) - assert_equal "<errors>", xml.first(8) - assert xml.include?("<error>Title is Wrong Create</error>") - assert xml.include?("<error>Content Empty</error>") - end - - def test_validation_order - Topic.validates_presence_of :title - Topic.validates_length_of :title, :minimum => 2 - - t = Topic.new("title" => "") - assert !t.valid? - assert_equal "can't be blank", t.errors.on("title").first - end - - def test_invalid_should_be_the_opposite_of_valid - Topic.validates_presence_of :title - - t = Topic.new - assert t.invalid? - assert t.errors.invalid?(:title) - - t.title = 'Things are going to change' - assert !t.invalid? - end - - # previous implementation of validates_presence_of eval'd the - # string with the wrong binding, this regression test is to - # ensure that it works correctly - def test_validation_with_if_as_string - Topic.validates_presence_of(:title) - Topic.validates_presence_of(:author_name, :if => "title.to_s.match('important')") - - t = Topic.new - assert !t.valid?, "A topic without a title should not be valid" - assert !t.errors.invalid?("author_name"), "A topic without an 'important' title should not require an author" - - t.title = "Just a title" - assert t.valid?, "A topic with a basic title should be valid" - - t.title = "A very important title" - assert !t.valid?, "A topic with an important title, but without an author, should not be valid" - assert t.errors.invalid?("author_name"), "A topic with an 'important' title should require an author" - - t.author_name = "Hubert J. Farnsworth" - assert t.valid?, "A topic with an important title and author should be valid" - end -end - - -class ValidatesNumericalityTest < ActiveRecord::TestCase - NIL = [nil] - BLANK = ["", " ", " \t \r \n"] - BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits - FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5) - INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) - FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS - INTEGERS = [0, 10, -10] + INTEGER_STRINGS - BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } - JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"] - INFINITY = [1.0/0.0] + def test_deprecated_validation_instance_methods + tom = DeprecatedPerson.new - repair_validations(Topic) - - def test_default_validates_numericality_of - Topic.validates_numericality_of :approved - - invalid!(NIL + BLANK + JUNK) - valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) - end - - def test_validates_numericality_of_with_nil_allowed - Topic.validates_numericality_of :approved, :allow_nil => true - - invalid!(JUNK) - valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY) - end - - def test_validates_numericality_of_with_integer_only - Topic.validates_numericality_of :approved, :only_integer => true - - invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY) - valid!(INTEGERS) - end - - def test_validates_numericality_of_with_integer_only_and_nil_allowed - Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true - - invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY) - valid!(NIL + BLANK + INTEGERS) - end - - def test_validates_numericality_with_greater_than - Topic.validates_numericality_of :approved, :greater_than => 10 - - invalid!([-10, 10], 'must be greater than 10') - valid!([11]) - end - - def test_validates_numericality_with_greater_than_or_equal - Topic.validates_numericality_of :approved, :greater_than_or_equal_to => 10 - - invalid!([-9, 9], 'must be greater than or equal to 10') - valid!([10]) - end - - def test_validates_numericality_with_equal_to - Topic.validates_numericality_of :approved, :equal_to => 10 - - invalid!([-10, 11] + INFINITY, 'must be equal to 10') - valid!([10]) - end - - def test_validates_numericality_with_less_than - Topic.validates_numericality_of :approved, :less_than => 10 - - invalid!([10], 'must be less than 10') - valid!([-9, 9]) - end - - def test_validates_numericality_with_less_than_or_equal_to - Topic.validates_numericality_of :approved, :less_than_or_equal_to => 10 - - invalid!([11], 'must be less than or equal to 10') - valid!([-10, 10]) - end - - def test_validates_numericality_with_odd - Topic.validates_numericality_of :approved, :odd => true - - invalid!([-2, 2], 'must be odd') - valid!([-1, 1]) - end - - def test_validates_numericality_with_even - Topic.validates_numericality_of :approved, :even => true - - invalid!([-1, 1], 'must be even') - valid!([-2, 2]) - end - - def test_validates_numericality_with_greater_than_less_than_and_even - Topic.validates_numericality_of :approved, :greater_than => 1, :less_than => 4, :even => true - - invalid!([1, 3, 4]) - valid!([2]) - end - - def test_validates_numericality_with_numeric_message - Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than {{count}}" - topic = Topic.new("title" => "numeric test", "approved" => 10) - - assert !topic.valid? - assert_equal "smaller than 4", topic.errors.on(:approved) - - Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than {{count}}" - topic = Topic.new("title" => "numeric test", "approved" => 1) - - assert !topic.valid? - assert_equal "greater than 4", topic.errors.on(:approved) - end - - private - def invalid!(values, error=nil) - with_each_topic_approved_value(values) do |topic, value| - assert !topic.valid?, "#{value.inspect} not rejected as a number" - assert topic.errors.on(:approved) - assert_equal error, topic.errors.on(:approved) if error - end + assert_deprecated do + assert tom.invalid? + assert_equal ["always invalid", "invalid on create"], tom.errors[:name] end - def valid!(values) - with_each_topic_approved_value(values) do |topic, value| - assert topic.valid?, "#{value.inspect} not accepted as a number" - end - end + tom.save(false) - def with_each_topic_approved_value(values) - topic = Topic.new("title" => "numeric test", "content" => "whatever") - values.each do |value| - topic.approved = value - yield topic, value - end + assert_deprecated do + assert tom.invalid? + assert_equal ["always invalid", "invalid on update"], tom.errors[:name] end + end end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index eb68153bbe..4c2514b0b3 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -145,10 +145,13 @@ class Account < ActiveRecord::Base true end + validate :check_empty_credit_limit + protected - def validate - errors.add_on_empty "credit_limit" - end + + def check_empty_credit_limit + errors.add_on_empty "credit_limit" + end private diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 7f02403d5a..1122c7256f 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -52,10 +52,13 @@ module MyApplication i.belongs_to :nested_unqualified_billing_firm, :class_name => 'Nested::Firm' end + validate :check_empty_credit_limit + protected - def validate - errors.add_on_empty "credit_limit" - end + + def check_empty_credit_limit + errors.add_on_empty "credit_limit" + end end end end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 1c990acab6..616c07687c 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -11,26 +11,30 @@ class Reply < Topic attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read - def validate - errors.add("title", "Empty") unless attribute_present? "title" + validate :check_empty_title + validate_on_create :check_content_mismatch + validate_on_update :check_wrong_update + + def check_empty_title + errors[:title] << "Empty" unless attribute_present?("title") end def errors_on_empty_content - errors.add("content", "Empty") unless attribute_present? "content" + errors[:content] << "Empty" unless attribute_present?("content") end - def validate_on_create + def check_content_mismatch if attribute_present?("title") && attribute_present?("content") && content == "Mismatch" - errors.add("title", "is Content Mismatch") + errors[:title] << "is Content Mismatch" end end def title_is_wrong_create - errors.add("title", "is Wrong Create") if attribute_present?("title") && title == "Wrong Create" + errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create" end - def validate_on_update - errors.add("title", "is Wrong Update") if attribute_present?("title") && title == "Wrong Update" + def check_wrong_update + errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update" end end diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb index db9007060f..f0d30b1624 100644 --- a/activeresource/lib/active_resource.rb +++ b/activeresource/lib/active_resource.rb @@ -31,6 +31,13 @@ rescue LoadError end end +begin + require 'active_model' +rescue LoadError + $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" + require 'active_model' +end + require 'active_resource/formats' require 'active_resource/base' require 'active_resource/validations' diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb index 8d21f8adbb..9d3d45b010 100644 --- a/activeresource/lib/active_resource/validations.rb +++ b/activeresource/lib/active_resource/validations.rb @@ -3,203 +3,8 @@ module ActiveResource end # Active Resource validation is reported to and from this object, which is used by Base#save - # to determine whether the object is in a valid state to be saved. See usage example in Validations. - class Errors - include Enumerable - attr_reader :errors - - delegate :empty?, :to => :errors - - def initialize(base) # :nodoc: - @base, @errors = base, {} - end - - # Adds an error to the base object instead of any particular attribute. This is used - # to report errors that don't tie to any specific attribute, but rather to the object - # as a whole. These error messages don't get prepended with any field name when iterating - # with +each_full+, so they should be complete sentences. - # - # ==== Examples - # my_folder = Folder.find(1) - # my_folder.errors.add_to_base("You can't edit an existing folder") - # my_folder.errors.on_base - # # => "You can't edit an existing folder" - # - # my_folder.errors.add_to_base("This folder has been tagged as frozen") - # my_folder.valid? - # # => false - # my_folder.errors.on_base - # # => ["You can't edit an existing folder", "This folder has been tagged as frozen"] - # - def add_to_base(msg) - add(:base, msg) - end - - # Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter) - # with the error message in +msg+. - # - # ==== Examples - # my_resource = Node.find(1) - # my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base' - # my_resource.errors.on('name') - # # => 'can not be "base"!' - # - # my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == '' - # my_resource.valid? - # # => false - # my_resource.errors.on('desc') - # # => 'can not be blank!' - # - def add(attribute, msg) - @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? - @errors[attribute.to_s] << msg - end - - # Returns true if the specified +attribute+ has errors associated with it. - # - # ==== Examples - # my_resource = Disk.find(1) - # my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main' - # my_resource.errors.on('location') - # # => 'must be Main!' - # - # my_resource.errors.invalid?('location') - # # => true - # my_resource.errors.invalid?('name') - # # => false - def invalid?(attribute) - !@errors[attribute.to_s].nil? - end - - # Returns +nil+ if no errors are associated with the specified +attribute+. - # Returns the error message if one error is associated with the specified +attribute+. - # Returns an array of error messages if more than one error is associated with the specified +attribute+. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # my_person.errors.on('login') - # # => nil - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.on('login') - # # => 'can not be empty' - # - # my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10 - # my_person.errors.on('login') - # # => ['can not be empty', 'can not be longer than 10 characters'] - def on(attribute) - errors = @errors[attribute.to_s] - return nil if errors.nil? - errors.size == 1 ? errors.first : errors - end - - alias :[] :on - - # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>. - # - # ==== Examples - # my_account = Account.find(1) - # my_account.errors.on_base - # # => nil - # - # my_account.errors.add_to_base("This account is frozen") - # my_account.errors.on_base - # # => "This account is frozen" - # - # my_account.errors.add_to_base("This account has been closed") - # my_account.errors.on_base - # # => ["This account is frozen", "This account has been closed"] - # - def on_base - on(:base) - end - - # Yields each attribute and associated message per error added. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # messages = '' - # my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"} - # messages - # # => "Login can not be empty<br />Password can not be empty<br />" - # - def each - @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } - end - - # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned - # through iteration as "First name can't be empty". - # - # ==== Examples - # my_person = Person.new(params[:person]) - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # messages = '' - # my_person.errors.each_full {|msg| messages += msg + "<br/>"} - # messages - # # => "Login can not be empty<br />Password can not be empty<br />" - # - def each_full - full_messages.each { |msg| yield msg } - end - - # Returns all the full error messages in an array. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # messages = '' - # my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"} - # messages - # # => "Login can not be empty<br />Password can not be empty<br />" - # - def full_messages - full_messages = [] - - @errors.each_key do |attr| - @errors[attr].each do |msg| - next if msg.nil? - - if attr == "base" - full_messages << msg - else - full_messages << [attr.humanize, msg].join(' ') - end - end - end - full_messages - end - - def clear - @errors = {} - end - - # Returns the total number of errors added. Two errors added to the same attribute will be counted as such - # with this as well. - # - # ==== Examples - # my_person = Person.new(params[:person]) - # my_person.errors.size - # # => 0 - # - # my_person.errors.add('login', 'can not be empty') if my_person.login == '' - # my_person.errors.add('password', 'can not be empty') if my_person.password == '' - # my_person.error.size - # # => 2 - # - def size - @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } - end - - alias_method :count, :size - alias_method :length, :size - + # to determine whether the object in a valid state to be saved. See usage example in Validations. + class Errors < ActiveModel::Errors # Grabs errors from the XML response. def from_xml(xml) clear @@ -212,7 +17,7 @@ module ActiveResource end end - add_to_base message if attr_message.nil? + self[:base] << message if attr_message.nil? end end end @@ -233,7 +38,7 @@ module ActiveResource # person.errors.empty? # => false # person.errors.count # => 1 # person.errors.full_messages # => ["Last name can't be empty"] - # person.errors.on(:last_name) # => "can't be empty" + # person.errors[:last_name] # => ["can't be empty"] # person.last_name = "Halpert" # person.save # => true (and person is now saved to the remote service) # diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb index 0f11ea482a..ce9371d050 100644 --- a/activeresource/test/abstract_unit.rb +++ b/activeresource/test/abstract_unit.rb @@ -14,6 +14,9 @@ require 'setter_trap' ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log") +# Show backtraces for deprecated behavior for quicker cleanup. +ActiveSupport::Deprecation.debug = true + def uses_gem(gem_name, test_name, version = '> 0') gem gem_name.to_s, version require gem_name.to_s diff --git a/activeresource/test/base_errors_test.rb b/activeresource/test/base_errors_test.rb index 7ae92c7d98..28813821df 100644 --- a/activeresource/test/base_errors_test.rb +++ b/activeresource/test/base_errors_test.rb @@ -20,21 +20,21 @@ class BaseErrorsTest < Test::Unit::TestCase end def test_should_parse_errors_to_individual_attributes - assert @person.errors.invalid?(:name) - assert_equal "can't be blank", @person.errors.on(:age) + assert @person.errors[:name].any? + assert_equal ["can't be blank"], @person.errors[:age] assert_equal ["can't be blank", "must start with a letter"], @person.errors[:name] - assert_equal "Person quota full for today.", @person.errors.on_base + assert_equal ["Person quota full for today."], @person.errors[:base] end def test_should_iterate_over_errors errors = [] - @person.errors.each { |attribute, message| errors << [attribute, message] } + @person.errors.each { |attribute, message| errors << [attribute.to_s, message] } assert errors.include?(["name", "can't be blank"]) end def test_should_iterate_over_full_errors errors = [] - @person.errors.each_full { |message| errors << message } + @person.errors.to_a.each { |message| errors << message } assert errors.include?("Name can't be blank") end |