Skip to content

Commit

Permalink
Uniqueness validation now works with localized fields. Fixes #1953.
Browse files Browse the repository at this point in the history
  • Loading branch information
durran committed May 5, 2012
1 parent 6cd1e55 commit cf3b427
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -770,6 +770,8 @@ For instructions on upgrading to newer versions, visit

### Resolved Issues

* \#1953 Uniqueness validation now works on localized fields.

* \#1936 Allow setting n levels deep embedded documents atomically without
conflicting mods when not using nested attributes or documents themselves
in an update call from the parent.
Expand Down
116 changes: 97 additions & 19 deletions lib/mongoid/validations/uniqueness.rb
Expand Up @@ -44,32 +44,36 @@ def validate_each(document, attribute, value)
attrib, val = to_validate(document, attribute, value)
return unless validation_required?(document, attrib)
if document.embedded?
return if skip_validation?(document)
relation = document._parent.send(document.metadata.name)
criteria = relation.where(criterion(document, attrib, val))
criteria = scope(criteria, document, attrib)
if criteria.count > 1
document.errors.add(
attrib,
:taken,
options.except(:case_sensitive, :scope).merge(val: val)
)
end
validate_embedded(document, attrib, val)
else
criteria = klass.unscoped.where(criterion(document, attrib, val))
criteria = scope(criteria, document, attrib)
if criteria.exists?
document.errors.add(
attrib, :taken, options.except(:case_sensitive, :scope).merge(val: val)
)
end
validate_root(document, attrib, val)
end
end

protected
private

# Add the error to the document.
#
# @api private
#
# @example Add the error.
# validator.add_error(doc, :name, "test")
#
# @param [ Document ] document The document to validate.
# @param [ Symbol ] attribute The name of the attribute.
# @param [ Object ] value The value of the object.
#
# @since 2.4.10
def add_error(document, attribute, value)
document.errors.add(
attribute, :taken, options.except(:case_sensitive, :scope).merge(value: value)
)
end

# Should the uniqueness validation be case sensitive?
#
# @api private
#
# @example Is the validation case sensitive?
# validator.case_sensitive?
#
Expand All @@ -80,8 +84,36 @@ def case_sensitive?
!(options[:case_sensitive] == false)
end

# Create the validation criteria.
#
# @api private
#
# @example Create the criteria.
# validator.create_criteria(User, user, :name, "syd")
#
# @param [ Class, Proxy ] base The base to execute the criteria from.
# @param [ Document ] document The document to validate.
# @param [ Symbol ] attribute The name of the attribute.
# @param [ Object ] value The value of the object.
#
# @return [ Criteria ] The criteria.
#
# @since 2.4.10
def create_criteria(base, document, attribute, value)
field = document.fields[attribute.to_s]
criteria = base.unscoped
if field.try(:localized?)
criteria.selector.update(criterion(document, attribute, value))
else
criteria = criteria.where(criterion(document, attribute, value))
end
scope(criteria, document, attribute)
end

# Get the default criteria for checking uniqueness.
#
# @api private
#
# @example Get the criteria.
# validator.criterion(person, :title, "Sir")
#
Expand All @@ -102,6 +134,8 @@ def criterion(document, attribute, value)

# Filter the value based on whether the check is case sensitive or not.
#
# @api private
#
# @example Filter the value.
# validator.filter("testing")
#
Expand All @@ -116,6 +150,8 @@ def filter(value)

# Scope the criteria to the scope options provided.
#
# @api private
#
# @example Scope the criteria.
# validator.scope(criteria, document)
#
Expand All @@ -134,6 +170,8 @@ def scope(criteria, document, attribute)

# Should validation be skipped?
#
# @api private
#
# @example Should the validation be skipped?
# validator.skip_validation?(doc)
#
Expand All @@ -148,6 +186,8 @@ def skip_validation?(document)

# Scope reference has changed?
#
# @api private
#
# @example Has scope reference changed?
# validator.scope_value_changed?(doc)
#
Expand All @@ -167,6 +207,8 @@ def scope_value_changed?(document)
# we need to send the key name and value to the db, not the relation
# object.
#
# @api private
#
# @example Get the name and key to validate.
# validator.to_validate(doc, :parent, Parent.new)
#
Expand All @@ -186,6 +228,42 @@ def to_validate(document, attribute, value)
end
end

# Validate an embedded document.
#
# @api private
#
# @example Validate the embedded document.
# validator.validate_embedded(doc, :name, "test")
#
# @param [ Document ] document The document.
# @param [ Symbol ] attribute The attribute name.
# @param [ Object ] value The value.
#
# @since 2.4.10
def validate_embedded(document, attribute, value)
return if skip_validation?(document)
relation = document._parent.send(document.metadata.name)
criteria = create_criteria(relation, document, attribute, value)
add_error(document, attribute, value) if criteria.count > 1
end

# Validate a root document.
#
# @api private
#
# @example Validate the root document.
# validator.validate_root(doc, :name, "test")
#
# @param [ Document ] document The document.
# @param [ Symbol ] attribute The attribute name.
# @param [ Object ] value The value.
#
# @since 2.4.10
def validate_root(document, attribute, value)
criteria = create_criteria(klass, document, attribute, value)
add_error(document, attribute, value) if criteria.exists?
end

# Are we required to validate the document?
#
# @example Is validation needed?
Expand Down
1 change: 1 addition & 0 deletions spec/app/models/dictionary.rb
Expand Up @@ -4,5 +4,6 @@ class Dictionary
field :publisher, type: String
field :year, type: Integer
field :published, type: Time
field :description, type: String, localize: true
has_many :words, validate: false
end
34 changes: 34 additions & 0 deletions spec/mongoid/validations/uniqueness_spec.rb
Expand Up @@ -34,6 +34,40 @@
end
end

context "when the field is localized" do

before do
Dictionary.validates_uniqueness_of :description
end

after do
Dictionary.reset_callbacks(:validate)
end

context "when the attribute is not unique" do

context "when the document is not the match" do

before do
Dictionary.create(description: "english")
end

let(:dictionary) do
Dictionary.new(description: "english")
end

it "returns false" do
dictionary.should_not be_valid
end

it "adds the uniqueness error" do
dictionary.valid?
dictionary.errors[:description].should eq([ "is already taken" ])
end
end
end
end

context "when no scope is provided" do

before do
Expand Down

0 comments on commit cf3b427

Please sign in to comment.