Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 9160

Show
Ignore:
Timestamp:
03/31/08 01:50:07 (3 months ago)
Author:
bitsweat
Message:

Fix case-sensitive validates_uniqueness_of. Closes #11366 [miloops]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/lib/active_record/validations.rb

    r9158 r9160  
    603603      # Configuration options: 
    604604      # * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken") 
    605       # * <tt>scope</tt> - One or more columns by which to limit the scope of the uniquness constraint. 
    606       # * <tt>case_sensitive</tt> - Looks for an exact match.  Ignored by non-text columns (true by default). 
     605      # * <tt>scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. 
     606      # * <tt>case_sensitive</tt> - Looks for an exact match.  Ignored by non-text columns (false by default). 
    607607      # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false) 
    608608      # * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is: false) 
     
    614614      #   method, proc or string should return or evaluate to a true or false value. 
    615615      def validates_uniqueness_of(*attr_names) 
    616         configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true
     616        configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken]
    617617        configuration.update(attr_names.extract_options!) 
    618618 
    619619        validates_each(attr_names,configuration) do |record, attr_name, value| 
    620           if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?) 
    621             condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" 
    622             condition_params = [value] 
    623           else 
    624             condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" 
    625             condition_params = [value.downcase] 
    626           end 
    627  
    628           if scope = configuration[:scope] 
    629             Array(scope).map do |scope_item| 
    630               scope_value = record.send(scope_item) 
    631               condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}" 
    632               condition_params << scope_value 
    633             end 
    634           end 
    635  
    636           unless record.new_record? 
    637             condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" 
    638             condition_params << record.send(:id) 
    639           end 
    640  
    641620          # The check for an existing value should be run from a class that 
    642621          # isn't abstract. This means working down from the current class 
     
    653632          finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } 
    654633 
    655           if finder_class.find(:first, :conditions => [condition_sql, *condition_params]) 
    656             record.errors.add(attr_name, configuration[:message]) 
     634          if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?) 
     635            condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" 
     636            condition_params = [value] 
     637          else 
     638            # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't. 
     639            # Hence, this is needed only for sqlite. 
     640            condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" 
     641            condition_params = [value.downcase] 
     642          end 
     643 
     644          if scope = configuration[:scope] 
     645            Array(scope).map do |scope_item| 
     646              scope_value = record.send(scope_item) 
     647              condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}" 
     648              condition_params << scope_value 
     649            end 
     650          end 
     651 
     652          unless record.new_record? 
     653            condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" 
     654            condition_params << record.send(:id) 
     655          end 
     656 
     657          results = connection.select_all( 
     658            construct_finder_sql( 
     659              :select     => "#{attr_name}", 
     660              :from       => "#{finder_class.quoted_table_name}", 
     661              :conditions => [condition_sql, *condition_params] 
     662            ) 
     663          ) 
     664 
     665          unless results.length.zero? 
     666            found = true 
     667 
     668            # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate 
     669            # column in ruby when case sensitive option 
     670            if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? 
     671              found = results.any? { |a| a[attr_name.to_s] == value } 
     672            end 
     673 
     674            record.errors.add(attr_name, configuration[:message]) if found 
    657675          end 
    658676        end 
  • trunk/activerecord/test/cases/validations_test.rb

    r9158 r9160  
    436436    assert t2.valid?, "should validate with nil" 
    437437    assert t2.save, "should save with nil" 
     438  end 
     439 
     440  def test_validate_case_sensitive_uniqueness 
     441    Topic.validates_uniqueness_of(:title, :case_sensitive => true, :allow_nil => true) 
     442 
     443    t = Topic.new("title" => "I'm unique!") 
     444    assert t.save, "Should save t as unique" 
     445 
     446    t.content = "Remaining unique" 
     447    assert t.save, "Should still save t as unique" 
     448 
     449    t2 = Topic.new("title" => "I'M UNIQUE!") 
     450    assert t2.valid?, "Should be valid" 
     451    assert t2.save, "Should save t2 as unique" 
     452    assert !t2.errors.on(:title) 
     453    assert !t2.errors.on(:parent_id) 
     454    assert_not_equal "has already been taken", t2.errors.on(:title) 
     455 
     456    t3 = Topic.new("title" => "I'M uNiQUe!") 
     457    assert t3.valid?, "Should be valid" 
     458    assert t3.save, "Should save t2 as unique" 
     459    assert !t3.errors.on(:title) 
     460    assert !t3.errors.on(:parent_id) 
     461    assert_not_equal "has already been taken", t3.errors.on(:title) 
    438462  end 
    439463