# Will ensure the set of provided attributes is unique - ie: no other record
# should have the same combination of values for the same attributes.
#
# Example:
# validates_uniqueness_of_set :first_name, :last_name
#
# Written by Pat Allan (http://freelancing-gods.com), and based heavily on
# Rails' validates_uniqueness_of code.
#
def validates_uniqueness_of_set(*attr_names)
configuration = {
:message => ActiveRecord::Errors.default_error_messages[:taken],
:case_sensitive => true
}
configuration.update(attr_names.extract_options!)
validate do |record|
condition_sql = []
condition_params = []
attr_names.each do |attr_name|
value = record.send(attr_name)
if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
condition_sql << "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params << value
else
condition_sql << "LOWER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params << value.downcase
end
end
if scope = configuration[:scope]
Array(scope).map do |scope_item|
scope_value = record.send(scope_item)
condition_sql << " #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}"
condition_params << scope_value
end
end
unless record.new_record?
condition_sql << " #{record.class.table_name}.#{record.class.primary_key} <> ?"
condition_params << record.send(:id)
end
# 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 finder_class.find(:first, :conditions => [condition_sql.join(" AND "), *condition_params])
record.errors.add(attr_names.first, configuration[:message])
end
end
end