diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index ac71b9d1dcddad1a2223c79be077e84d6b871eb9..014500fa28b8b5d19cb1d47598ac0a07e9a6e733 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,4 +1,23 @@
## Rails 4.0.0 (unreleased) ##
+* Add `ActiveModel::Validations::AbsenceValidator`, a validator to check the
+ absence of attributes.
+
+ class Person < ActiveRecord::Base
+ validates_absence_of :first_name
+ end
+
+ person = Person.new
+ person.first_name = "John"
+ person.valid?
+ => false
+ # first_name must be blank
+
+ * Roberto Vasquez Angel*
+
+* Added `ActiveModel::Errors#add_on_present` method. Adds error messages to
+ present attributes.
+
+ *Roberto Vasquez Angel*
* `[attribute]_changed?` now returns `false` after a call to `reset_[attribute]!`
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 963e52bff3544a4f6f670ee82c9583efb1e7edd1..b713e99e25ad32b23dc465c6bb4e748e4b8926e1 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -328,6 +328,19 @@ def add_on_blank(attributes, options = {})
end
end
+ # Will add an error message to each of the attributes in +attributes+ that
+ # is present (using Object#present?).
+ #
+ # person.errors.add_on_present(:name)
+ # person.errors.messages
+ # # => { :name => ["must be blank"] }
+ def add_on_present(attributes, options = {})
+ Array(attributes).flatten.each do |attribute|
+ value = @base.send(:read_attribute_for_validation, attribute)
+ add(attribute, :not_blank, options) if value.present?
+ end
+ end
+
# Returns +true+ if an error on the attribute with the given message is
# present, +false+ otherwise. +message+ is treated the same as for +add+.
#
diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml
index d17848c861f56c827ab91519bd2568c342bf4705..8ea34b84f5fc04e306421ac77779e9efe6a2d45b 100644
--- a/activemodel/lib/active_model/locale/en.yml
+++ b/activemodel/lib/active_model/locale/en.yml
@@ -13,6 +13,7 @@ en:
accepted: "must be accepted"
empty: "can't be empty"
blank: "can't be blank"
+ not_blank: "must 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)"
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6790554907c82057f2272b6843331fab09cb33b8
--- /dev/null
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -0,0 +1,31 @@
+module ActiveModel
+ module Validations
+ # == Active Model Absence Validator
+ class AbsenceValidator < EachValidator #:nodoc:
+ def validate(record)
+ record.errors.add_on_present(attributes, options)
+ end
+ end
+
+ module HelperMethods
+ # Validates that the specified attributes are blank (as defined by
+ # Object#blank?). Happens by default on save.
+ #
+ # class Person < ActiveRecord::Base
+ # validates_absence_of :first_name
+ # end
+ #
+ # The first_name attribute must be in the object and it must be blank.
+ #
+ # Configuration options:
+ # * :message - A custom error message (default is: "must be blank").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+ and +:strict+.
+ # See ActiveModel::Validation#validates for more information
+ def validates_absence_of(*attr_names)
+ validates_with AbsenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c05d71de5a318aeda41a61ef60b1403882e85b09
--- /dev/null
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -0,0 +1,67 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'models/topic'
+require 'models/person'
+require 'models/custom_reader'
+
+class AbsenceValidationTest < ActiveModel::TestCase
+ teardown do
+ Topic.reset_callbacks(:validate)
+ Person.reset_callbacks(:validate)
+ CustomReader.reset_callbacks(:validate)
+ end
+
+ def test_validate_absences
+ Topic.validates_absence_of(:title, :content)
+ t = Topic.new
+ t.title = "foo"
+ t.content = "bar"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:title]
+ assert_equal ["must be blank"], t.errors[:content]
+ t.title = ""
+ t.content = "something"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:content]
+ t.content = ""
+ assert t.valid?
+ end
+
+ def test_accepts_array_arguments
+ Topic.validates_absence_of %w(title content)
+ t = Topic.new
+ t.title = "foo"
+ t.content = "bar"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:title]
+ assert_equal ["must be blank"], t.errors[:content]
+ end
+
+ def test_validates_acceptance_of_with_custom_error_using_quotes
+ Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes"
+ p = Person.new
+ p.karma = "good"
+ assert p.invalid?
+ assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
+ end
+
+ def test_validates_absence_of_for_ruby_class
+ Person.validates_absence_of :karma
+ p = Person.new
+ p.karma = "good"
+ assert p.invalid?
+ assert_equal ["must be blank"], p.errors[:karma]
+ p.karma = nil
+ assert p.valid?
+ end
+
+ def test_validates_absence_of_for_ruby_class_with_custom_reader
+ CustomReader.validates_absence_of :karma
+ p = CustomReader.new
+ p[:karma] = "excellent"
+ assert p.invalid?
+ assert_equal ["must be blank"], p.errors[:karma]
+ p[:karma] = ""
+ assert p.valid?
+ end
+end