module MongoModel
module DocumentExtensions
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 < MongoModel::Document
# 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 < MongoModel::Document
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
# end
#
# When the document is created, a check is performed to make sure that no document exists in the database with the given value for the specified
# attribute (that maps to a property). When the document is updated, the same check is made but disregarding the document itself.
#
# Configuration options:
# * :message - Specifies a custom error message (default is: "has already been taken").
# * :scope - One or more properties by which to limit the scope of the uniqueness constraint.
# * :case_sensitive - Looks for an exact match. Ignored by non-text columns (+true+ by default).
# * :allow_nil - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * :allow_blank - If set to true, skips this validation if the attribute is blank (default is +false+).
# * :if - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
# * :unless - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
#
# === Concurrency and integrity
#
# Note that this validation method does not have the same race condition suffered by ActiveRecord and other ORMs.
# A unique index is added to the collection to ensure that the collection never ends up in an invalid state.
def validates_uniqueness_of(*attr_names)
configuration = { :case_sensitive => true }
configuration.update(attr_names.extract_options!)
# Enable safety checks on save
self.save_safely = true
# Create unique indexes to deal with race condition
attr_names.each do |attr_name|
if configuration[:case_sensitive]
index *[attr_name] + Array(configuration[:scope]) << { :unique => true }
else
lowercase_key = "_lowercase_#{attr_name}"
before_save { attributes[lowercase_key] = send(attr_name).downcase }
index *[lowercase_key] + Array(configuration[:scope]) << { :unique => true }
end
end
validates_each(attr_names, configuration) do |record, attr_name, value|
unique_scope = scoped
if configuration[:case_sensitive] || !value.is_a?(String)
unique_scope = unique_scope.where(attr_name => value)
else
unique_scope = unique_scope.where("_lowercase_#{attr_name}" => value.downcase)
end
Array(configuration[:scope]).each do |scope|
unique_scope = unique_scope.where(scope => record.send(scope))
end
unique_scope = unique_scope.where(:id.ne => record.id) unless record.new_record?
if unique_scope.any?
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
end
end
end
end
end
end
end