diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index a914b0c2765..0d92e8d5747 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -14,6 +14,21 @@ * Added Base.validate_presence as an alternative to implementing validate and doing errors.add_on_empty yourself. +* Added Base.validate_uniqueness that alidates whether the value of the specified attributes are unique across the system. + Useful for making sure that only one user can be named "davidhh". + + Model: + class Person < ActiveRecord::Base + validate_uniqueness :user_name + end + + View: + <%= text_field "person", "user_name" %> + + When the record is created, a check is performed to make sure that no record exist in the database with the given value for the specified + attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. + + * Added Base.validate_confirmation that encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: Model: diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc76204dc07..2346aa19e9e 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -665,7 +665,7 @@ module ActiveRecord #:nodoc: end until values.empty? - statement.sub!(/\?/, connection.quote(values.shift)) + statement.sub!(/\?/, encode_quoted_value(values.shift)) end statement.gsub('?') { |all, match| connection.quote(values.shift) } @@ -674,7 +674,7 @@ module ActiveRecord #:nodoc: def replace_named_bind_variables(statement, values_hash) orig_statement = statement.clone values_hash.keys.each do |k| - if statement.sub!(/:#{k.id2name}/, connection.quote(values_hash.delete(k))).nil? + if statement.sub!(/:#{k.id2name}/, encode_quoted_value(values_hash.delete(k))).nil? raise PreparedStatementInvalid, ":#{k} is not a variable in [#{orig_statement}]" end end @@ -685,6 +685,12 @@ module ActiveRecord #:nodoc: return statement end + + def encode_quoted_value(value) + quoted_value = connection.quote(value) + quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") + quoted_value + end end public diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 1a260575283..87bad183abd 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -324,7 +324,7 @@ module ActiveRecord def quote(value, column = nil) case value - when String then "'#{quote_string(value)}'" # ' (for ruby-mode) + when String then %('#{quote_string(value)}') # ' (for ruby-mode) when NilClass then "NULL" when TrueClass then (column && column.type == :boolean ? "'t'" : "1") when FalseClass then (column && column.type == :boolean ? "'f'" : "0") diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 17810318b7a..b2e7b1e99bd 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -74,7 +74,6 @@ module ActiveRecord # situations. def validate_confirmation(*attr_names) error_message = attr_names.last.is_a?(String) ? attr_names.pop : "doesn't match confirmation" - validation_method = block_given? ? yield : "validate" for attr_name in attr_names @@ -111,7 +110,6 @@ module ActiveRecord # NOTE: The agreement is considered valid if it's set to the string "1". This makes it easy to relate it to an HTML checkbox. def validate_acceptance(*attr_names) error_message = attr_names.last.is_a?(String) ? attr_names.pop : "must be accepted" - validation_method = block_given? ? yield : "validate" for attr_name in attr_names @@ -132,7 +130,6 @@ module ActiveRecord def validate_presence(*attr_names) error_message = attr_names.last.is_a?(String) ? attr_names.pop : "can't be empty" - validation_method = block_given? ? yield : "validate" for attr_name in attr_names @@ -149,6 +146,27 @@ module ActiveRecord def validate_presence_on_update(*attr_names) validate_presence(*attr_names) { "validate_on_update" } end + + # 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". + # + # Model: + # class Person < ActiveRecord::Base + # validate_uniqueness :user_name + # end + # + # View: + # <%= text_field "person", "user_name" %> + # + # When the record is created, a check is performed to make sure that no record exist in the database with the given value for the specified + # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself. + def validate_uniqueness(*attr_names) + error_message = attr_names.last.is_a?(String) ? attr_names.pop : "has already been taken" + + for attr_name in attr_names + class_eval(%(validate %{errors.add("#{attr_name}", "#{error_message}") if self.class.find_first(new_record? ? ["#{attr_name} = ?", #{attr_name}] : ["#{attr_name} = ? AND id <> ?", #{attr_name}, id])})) + end + end end # The validation process on save can be skipped by passing false. The regular Base#save method is diff --git a/activerecord/test/finder_test.rb b/activerecord/test/finder_test.rb index ff0ab569090..ca78fbe651b 100755 --- a/activerecord/test/finder_test.rb +++ b/activerecord/test/finder_test.rb @@ -95,6 +95,16 @@ class FinderTest < Test::Unit::TestCase Company.find_first(["id=?", 2, 3, 4]) } end + + def test_bind_variables_with_quotes + Company.create("name" => "37signals' go'es agains") + assert Company.find_first(["name = ?", "37signals' go'es agains"]) + end + + def test_named_bind_variables_with_quotes + Company.create("name" => "37signals' go'es agains") + assert Company.find_first(["name = :name", {:name => "37signals' go'es agains"}]) + end def test_named_bind_variables assert_kind_of Firm, Company.find_first(["name = :name", { :name => "37signals" }]) diff --git a/activerecord/test/validations_test.rb b/activerecord/test/validations_test.rb index 2d6c795d92b..1abdfa062d2 100755 --- a/activerecord/test/validations_test.rb +++ b/activerecord/test/validations_test.rb @@ -172,4 +172,22 @@ class ValidationsTest < Test::Unit::TestCase assert t.save end + + def test_validate_uniqueness + Topic.validate_uniqueness(:title) + + t = Topic.new("title" => "I'm unique!") + assert t.save, "Should save t as unique" + + t.content = "Remaining unique" + assert t.save, "Should still save t as unique" + + t2 = Topic.new("title" => "I'm unique!") + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert_equal "has already been taken", t2.errors.on(:title) + + t2.title = "Now Im really also unique" + assert t2.save, "Should now save t2 as unique" + end end \ No newline at end of file