aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG15
-rwxr-xr-xactiverecord/lib/active_record/base.rb10
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb2
-rwxr-xr-xactiverecord/lib/active_record/validations.rb24
-rwxr-xr-xactiverecord/test/finder_test.rb10
-rwxr-xr-xactiverecord/test/validations_test.rb18
6 files changed, 73 insertions, 6 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index a914b0c276..0d92e8d574 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 cc76204dc0..2346aa19e9 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 1a26057528..87bad183ab 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 17810318b7..b2e7b1e99b 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 ff0ab56909..ca78fbe651 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 2d6c795d92..1abdfa062d 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