aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG4
-rw-r--r--activerecord/lib/active_record.rb22
-rw-r--r--activerecord/lib/active_record/association_preload.rb8
-rwxr-xr-x[-rw-r--r--]activerecord/lib/active_record/associations.rb22
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb4
-rwxr-xr-xactiverecord/lib/active_record/base.rb186
-rw-r--r--activerecord/lib/active_record/calculations.rb19
-rw-r--r--activerecord/lib/active_record/callbacks.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb19
-rw-r--r--activerecord/lib/active_record/dirty.rb5
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb40
-rw-r--r--activerecord/lib/active_record/locale/en-US.rb25
-rw-r--r--activerecord/lib/active_record/locale/en-US.yml33
-rw-r--r--activerecord/lib/active_record/migration.rb47
-rw-r--r--activerecord/lib/active_record/named_scope.rb6
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/test_case.rb2
-rw-r--r--activerecord/lib/active_record/transactions.rb16
-rw-r--r--activerecord/lib/active_record/validations.rb131
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb12
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb42
-rw-r--r--activerecord/test/cases/associations/eager_test.rb19
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb18
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb49
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb7
-rw-r--r--activerecord/test/cases/associations_test.rb4
-rw-r--r--activerecord/test/cases/base_test.rb12
-rw-r--r--activerecord/test/cases/column_definition_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb36
-rw-r--r--activerecord/test/cases/finder_test.rb56
-rw-r--r--activerecord/test/cases/helper.rb1
-rw-r--r--activerecord/test/cases/i18n_test.rb41
-rw-r--r--activerecord/test/cases/migration_test.rb57
-rw-r--r--activerecord/test/cases/named_scope_test.rb14
-rw-r--r--activerecord/test/cases/query_cache_test.rb5
-rw-r--r--activerecord/test/cases/reflection_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb62
-rw-r--r--activerecord/test/cases/validations_i18n_test.rb533
-rw-r--r--activerecord/test/cases/validations_test.rb8
-rw-r--r--activerecord/test/connections/native_mysql/connection.rb4
-rw-r--r--activerecord/test/migrations/broken/100_migration_that_raises_exception.rb10
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--activerecord/test/models/developer.rb2
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb4
-rw-r--r--activerecord/test/schema/schema.rb2
58 files changed, 1251 insertions, 477 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 90be9b700a..12295ac799 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,9 @@
*Edge*
+* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
+
+* Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
+
* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
* change_column_default preserves the not-null constraint. #617 [Tarmo Tänav]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 17a7949959..a6bbd6fc82 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -21,17 +21,12 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-$:.unshift(File.dirname(__FILE__)) unless
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
-
-unless defined? ActiveSupport
- active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
- if File.exist?(active_support_path)
- $:.unshift active_support_path
- require 'active_support'
- else
- require 'rubygems'
- gem 'activesupport'
+begin
+ require 'active_support'
+rescue LoadError
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
+ if File.directory?(activesupport_path)
+ $:.unshift activesupport_path
require 'active_support'
end
end
@@ -56,6 +51,7 @@ require 'active_record/calculations'
require 'active_record/serialization'
require 'active_record/attribute_methods'
require 'active_record/dirty'
+require 'active_record/dynamic_finder_match'
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
@@ -81,7 +77,5 @@ require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'
-I18n.backend.populate do
- require 'active_record/locale/en-US.rb'
-end
+I18n.load_translations File.dirname(__FILE__) + '/active_record/locale/en-US.yml'
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index c7594809b7..61fa34ac39 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -51,9 +51,7 @@ module ActiveRecord
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
- association_proxy = parent_record.send(reflection_name)
- association_proxy.loaded
- association_proxy.target = associated_record
+ parent_record.send("set_#{reflection_name}_target", associated_record)
end
end
@@ -112,8 +110,8 @@ module ActiveRecord
def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
+ records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
- records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
@@ -126,8 +124,6 @@ module ActiveRecord
end
end
else
- records.each {|record| record.send("set_#{reflection.name}_target", nil)}
-
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index eb1281901b..1af8ddcaf8 100644..100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1269,10 +1269,9 @@ module ActiveRecord
association.create_through_record(new_value)
self.send(reflection.name, new_value)
else
- association.replace(new_value)
+ association.replace(new_value)
+ instance_variable_set(ivar, new_value.nil? ? nil : association)
end
-
- instance_variable_set(ivar, new_value.nil? ? nil : association)
end
define_method("set_#{reflection.name}_target") do |target|
@@ -1678,19 +1677,19 @@ module ActiveRecord
else all << cond
end
end
- conditions.join(' ').scan(/([\.\w]+).?\./).flatten
+ conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
end
def order_tables(options)
order = [options[:order], scope(:find, :order) ].join(", ")
return [] unless order && order.is_a?(String)
- order.scan(/([\.\w]+).?\./).flatten
+ order.scan(/([\.a-zA-Z_]+).?\./).flatten
end
def selects_tables(options)
select = options[:select]
return [] unless select && select.is_a?(String)
- select.scan(/"?([\.\w]+)"?.?\./).flatten
+ select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
end
# Checks if the conditions reference a table other than the current model table
@@ -1892,6 +1891,7 @@ module ActiveRecord
collection.target.push(association)
when :has_one
return if record.id.to_s != join.parent.record_id(row).to_s
+ return if record.instance_variable_defined?("@#{join.reflection.name}")
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
record.send("set_#{join.reflection.name}_target", association)
when :belongs_to
@@ -1973,7 +1973,7 @@ module ActiveRecord
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
end
- if reflection.macro == :has_many && reflection.options[:through]
+ if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
@@ -1997,7 +1997,7 @@ module ActiveRecord
]
when :has_many, :has_one
case
- when reflection.macro == :has_many && reflection.options[:through]
+ when reflection.options[:through]
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
@@ -2098,10 +2098,8 @@ module ActiveRecord
else
""
end || ''
- join << %(AND %s.%s = %s ) % [
- connection.quote_table_name(aliased_table_name),
- connection.quote_column_name(klass.inheritance_column),
- klass.quote_value(klass.sti_name)] unless klass.descends_from_active_record?
+ join << %(AND %s) % [
+ klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref|
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 78b4c137a7..acdcd14ec8 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -74,8 +74,8 @@ module ActiveRecord
end
# Does the proxy or its \target respond to +symbol+?
- def respond_to?(symbol, include_priv = false)
- proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
+ def respond_to?(*args)
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end
# Forwards <tt>===</tt> explicitly to the \target because the instance method
@@ -155,18 +155,6 @@ module ActiveRecord
records.map { |record| record.quoted_id }.join(',')
end
- # Interpolates the SQL in <tt>options[key]</tt> and assigns the result
- # back, for any +key+ in +keys+ that's present in +options+.
- #
- # Meant to be used like this:
- #
- # interpolate_sql_options!(@reflection.options, :finder_sql)
- #
- def interpolate_sql_options!(options, *keys)
- keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
- end
-
- # Forwards the call to the owner.
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
@@ -261,7 +249,7 @@ module ActiveRecord
# Array#flatten has problems with recursive arrays. Going one level
# deeper solves the majority of the problems.
def flatten_deeper(array)
- array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
end
# Returns the ID of the owner, quoted if needed.
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index d516d54151..e7e433b6b6 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -70,10 +70,8 @@ module ActiveRecord
end
def construct_sql
- interpolate_sql_options!(@reflection.options, :finder_sql)
-
if @reflection.options[:finder_sql]
- @finder_sql = @reflection.options[:finder_sql]
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index f06e69aba3..451bf8faa9 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -18,7 +18,16 @@ module ActiveRecord
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] ||= @reflection.options[:include]
- @reflection.klass.count(column_name, options)
+ value = @reflection.klass.count(column_name, options)
+
+ limit = @reflection.options[:limit]
+ offset = @reflection.options[:offset]
+
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
+ end
end
end
@@ -49,8 +58,11 @@ module ActiveRecord
else
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end
-
- @target = [] and loaded if count == 0
+
+ # If there's nothing in the database and @target has no new records
+ # we are certain the current target is an empty array. This is a
+ # documented side-effect of the method that may avoid an extra SELECT.
+ @target ||= [] and loaded if count == 0
if @reflection.options[:limit]
count = [ @reflection.options[:limit], count ].min
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index e1bfff5923..24b02efc35 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -237,7 +237,7 @@ module ActiveRecord
end
def build_sti_condition
- "#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}"
+ @reflection.through_reflection.klass.send(:type_condition)
end
alias_method :sql_conditions, :conditions
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index fdc0fa52c9..18733255d2 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -21,8 +21,8 @@ module ActiveRecord
def replace(obj, dont_save = false)
load_target
- unless @target.nil?
- if dependent? && !dont_save && @target != obj
+ unless @target.nil? || @target == obj
+ if dependent? && !dont_save
@target.destroy unless @target.new_record?
@owner.clear_association_cache
else
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index c846956e1f..b78bd5d931 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -22,6 +22,10 @@ module ActiveRecord
def find_target
super.first
+ end
+
+ def reset_target!
+ @target = nil
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b282ea931e..4a23c8c690 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1223,11 +1223,46 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
+ def self_and_descendents_from_active_record#nodoc:
+ klass = self
+ classes = [klass]
+ while klass != klass.base_class
+ classes << klass = klass.superclass
+ end
+ classes
+ rescue
+ # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
+ # Appearantly the method base_class causes some trouble.
+ # It now works for sure.
+ [self]
+ end
+
# Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
# Person.human_attribute_name("first_name") # => "First name"
- # Deprecated in favor of just calling "first_name".humanize
- def human_attribute_name(attribute_key_name) #:nodoc:
- attribute_key_name.humanize
+ # This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n
+ # module now.
+ # Specify +options+ with additional translating options.
+ def human_attribute_name(attribute_key_name, options = {})
+ defaults = self_and_descendents_from_active_record.map do |klass|
+ :"#{klass.name.underscore}.#{attribute_key_name}"
+ end
+ defaults << options[:default] if options[:default]
+ defaults.flatten!
+ defaults << attribute_key_name.humanize
+ options[:count] ||= 1
+ I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes]))
+ end
+
+ # Transform the modelname into a more humane format, using I18n.
+ # Defaults to the basic humanize method.
+ # Default scope of the translation is activerecord.models
+ # Specify +options+ with additional translating options.
+ def human_name(options = {})
+ defaults = self_and_descendents_from_active_record.map do |klass|
+ :"#{klass.name.underscore}"
+ end
+ defaults << self.name.humanize
+ I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
end
# True if this isn't a concrete subclass needing a STI type condition.
@@ -1322,8 +1357,8 @@ module ActiveRecord #:nodoc:
end
def respond_to?(method_id, include_private = false)
- if match = matches_dynamic_finder?(method_id) || matches_dynamic_finder_with_initialize_or_create?(method_id)
- return true if all_attributes_exists?(extract_attribute_names_from_match(match))
+ if match = DynamicFinderMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
end
super
end
@@ -1612,10 +1647,11 @@ module ActiveRecord #:nodoc:
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
end
- def type_condition
+ def type_condition(table_alias=nil)
+ quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
- type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
- condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
+ type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
+ condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
end
" (#{type_condition}) "
@@ -1641,88 +1677,67 @@ module ActiveRecord #:nodoc:
# Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
# attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments)
- if match = matches_dynamic_finder?(method_id)
- finder = determine_finder(match)
-
- attribute_names = extract_attribute_names_from_match(match)
+ if match = DynamicFinderMatch.match(method_id)
+ attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
-
- self.class_eval %{
- def self.#{method_id}(*args)
- options = args.extract_options!
- attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
- finder_options = { :conditions => attributes }
- validate_find_options(options)
- set_readonly_option!(options)
-
- if options[:conditions]
- with_scope(:find => finder_options) do
- ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
+ if match.finder?
+ finder = match.finder
+ bang = match.bang?
+ self.class_eval %{
+ def self.#{method_id}(*args)
+ options = args.extract_options!
+ attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ finder_options = { :conditions => attributes }
+ validate_find_options(options)
+ set_readonly_option!(options)
+
+ #{'result = ' if bang}if options[:conditions]
+ with_scope(:find => finder_options) do
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
+ end
+ else
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
end
- else
- ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
- end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
- elsif match = matches_dynamic_finder_with_initialize_or_create?(method_id)
- instantiator = determine_instantiator(match)
- attribute_names = extract_attribute_names_from_match(match)
- super unless all_attributes_exists?(attribute_names)
-
- self.class_eval %{
- def self.#{method_id}(*args)
- guard_protected_attributes = false
-
- if args[0].is_a?(Hash)
- guard_protected_attributes = true
- attributes = args[0].with_indifferent_access
- find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
- else
- find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ #{'result || raise(RecordNotFound)' if bang}
end
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ elsif match.instantiator?
+ instantiator = match.instantiator
+ self.class_eval %{
+ def self.#{method_id}(*args)
+ guard_protected_attributes = false
+
+ if args[0].is_a?(Hash)
+ guard_protected_attributes = true
+ attributes = args[0].with_indifferent_access
+ find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
+ else
+ find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ end
- options = { :conditions => find_attributes }
- set_readonly_option!(options)
+ options = { :conditions => find_attributes }
+ set_readonly_option!(options)
- record = find_initial(options)
+ record = find_initial(options)
- if record.nil?
- record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
- #{'yield(record) if block_given?'}
- #{'record.save' if instantiator == :create}
- record
- else
- record
+ if record.nil?
+ record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
+ #{'yield(record) if block_given?'}
+ #{'record.save' if instantiator == :create}
+ record
+ else
+ record
+ end
end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ end
else
super
end
end
- def matches_dynamic_finder?(method_id)
- /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
- end
-
- def matches_dynamic_finder_with_initialize_or_create?(method_id)
- /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
- end
-
- def determine_finder(match)
- match.captures.first == 'all_by' ? :find_every : :find_initial
- end
-
- def determine_instantiator(match)
- match.captures.first == 'initialize' ? :new : :create
- end
-
- def extract_attribute_names_from_match(match)
- match.captures.last.split('_and_')
- end
-
def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
@@ -1752,7 +1767,7 @@ module ActiveRecord #:nodoc:
def attribute_condition(argument)
case argument
when nil then "IS ?"
- when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)"
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
when Range then "BETWEEN ? AND ?"
else "= ?"
end
@@ -2594,11 +2609,14 @@ module ActiveRecord #:nodoc:
end
def convert_number_column_value(value)
- case value
- when FalseClass; 0
- when TrueClass; 1
- when ''; nil
- else value
+ if value == false
+ 0
+ elsif value == true
+ 1
+ elsif value.is_a?(String) && value.blank?
+ nil
+ else
+ value
end
end
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index e765b46cc2..26acbf53b5 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -186,11 +186,17 @@ module ActiveRecord
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} "
end
+
+ joins = ""
+ add_joins!(joins, options, scope)
+
if merged_includes.any?
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
- add_joins!(sql, options, scope)
+
+ sql << joins unless joins.blank?
+
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
@@ -260,7 +266,14 @@ module ActiveRecord
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
def column_alias_for(*keys)
- connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_'))
+ table_name = keys.join(' ')
+ table_name.downcase!
+ table_name.gsub!(/\*/, 'all')
+ table_name.gsub!(/\W+/, ' ')
+ table_name.strip!
+ table_name.gsub!(/ +/, '_')
+
+ connection.table_alias_for(table_name)
end
def column_for(field)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index d99e183f9e..dd7ae51096 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -172,16 +172,15 @@ module ActiveRecord
#
# == Transactions
#
- # The entire callback chain for +save+ and +destroy+ runs within their transaction, including
- # the <tt>after_*</tt> hooks. Cancellation does not trigger a rollback. To rollback
- # the transaction just raise an exception the same way you do for regular transactions.
- #
- # Note though that such an exception bypasses the regular call chain in Active Record:
- # If ActiveRecord::Rollback is raised both +save+ and +destroy+ return +nil+. On the other
- # hand <tt>save!</tt> does *not* raise ActiveRecord::RecordNotSaved, and does not raise
- # anything else for that matter, <tt>save!</tt> just returns +nil+ in that case.
- # If any other exception is raised it goes up until it reaches the caller, no matter
- # which one of the three actions was being performed.
+ # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
+ # within a transaction. That includes <tt>after_*</tt> hooks. If everything
+ # goes fine a COMMIT is executed once the chain has been completed.
+ #
+ # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
+ # can also trigger a ROLLBACK raising an exception in any of the callbacks,
+ # including <tt>after_*</tt> hooks. Note, however, that in that case the client
+ # needs to be aware of it because an ordinary +save+ will raise such exception
+ # instead of quietly returning +false+.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 31d6c7942c..75032efe57 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -138,7 +138,11 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
- TRUE_VALUES.include?(value)
+ if value.is_a?(String) && value.blank?
+ nil
+ else
+ TRUE_VALUES.include?(value)
+ end
end
# convert something to a BigDecimal
@@ -443,9 +447,10 @@ module ActiveRecord
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
- def timestamps
- column(:created_at, :datetime)
- column(:updated_at, :datetime)
+ def timestamps(*args)
+ options = args.extract_options!
+ column(:created_at, :datetime, options)
+ column(:updated_at, :datetime, options)
end
def references(*args)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 0f60a91ef1..bececf82a0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -384,12 +384,8 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
# must explicitly check for :null to allow change_column to work on migrations
- if options.has_key? :null
- if options[:null] == false
- sql << " NOT NULL"
- else
- sql << " NULL"
- end
+ if options[:null] == false
+ sql << " NOT NULL"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 47dbf5a5f3..6924bb7e6f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -51,6 +51,13 @@ module ActiveRecord
true
end
+ # Does this adapter support DDL rollbacks in transactions? That is, would
+ # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
+ # SQL Server, and others support this. MySQL and others do not.
+ def supports_ddl_transactions?
+ false
+ end
+
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 856435517a..0c2532f21d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -23,8 +23,8 @@ module ActiveRecord
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
- username = config[:username].to_s
- password = config[:password].to_s
+ username = config[:username].to_s if config[:username]
+ password = config[:password].to_s if config[:password]
if config.has_key?(:database)
database = config[:database]
@@ -335,6 +335,10 @@ module ActiveRecord
postgresql_version >= 80200
end
+ def supports_ddl_transactions?
+ true
+ end
+
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
@@ -376,7 +380,7 @@ module ActiveRecord
# There are some incorrectly compiled postgres drivers out there
# that don't define PGconn.escape.
self.class.instance_eval do
- undef_method(:quote_string)
+ remove_method(:quote_string)
end
end
quote_string(s)
@@ -534,13 +538,13 @@ module ActiveRecord
option_string = options.symbolize_keys.sum do |key, value|
case key
when :owner
- " OWNER = '#{value}'"
+ " OWNER = \"#{value}\""
when :template
- " TEMPLATE = #{value}"
+ " TEMPLATE = \"#{value}\""
when :encoding
" ENCODING = '#{value}'"
when :tablespace
- " TABLESPACE = #{value}"
+ " TABLESPACE = \"#{value}\""
when :connection_limit
" CONNECTION LIMIT = #{value}"
else
@@ -761,7 +765,8 @@ module ActiveRecord
begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- rescue ActiveRecord::StatementInvalid
+ rescue ActiveRecord::StatementInvalid => e
+ raise e if postgresql_version > 80000
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin
begin_db_transaction
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
index 63bf8c8f5b..7e246e62ca 100644
--- a/activerecord/lib/active_record/dirty.rb
+++ b/activerecord/lib/active_record/dirty.rb
@@ -123,7 +123,10 @@ module ActiveRecord
attr = attr.to_s
# The attribute already has an unsaved change.
- unless changed_attributes.include?(attr)
+ if changed_attributes.include?(attr)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless field_changed?(attr, old, value)
+ else
old = clone_attribute_value(:read_attribute, attr)
changed_attributes[attr] = old if field_changed?(attr, old, value)
end
diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb
new file mode 100644
index 0000000000..b105b919f5
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_finder_match.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ class DynamicFinderMatch
+ def self.match(method)
+ df_match = self.new(method)
+ df_match.finder ? df_match : nil
+ end
+
+ def initialize(method)
+ @finder = :find_initial
+ case method.to_s
+ when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
+ @finder = :find_every if $1 == 'all_by'
+ names = $2
+ when /^find_by_([_a-zA-Z]\w*)\!$/
+ @bang = true
+ names = $1
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
+ @instantiator = $1 == 'initialize' ? :new : :create
+ names = $2
+ else
+ @finder = nil
+ end
+ @attribute_names = names && names.split('_and_')
+ end
+
+ attr_reader :finder, :attribute_names, :instantiator
+
+ def finder?
+ !@finder.nil? && @instantiator.nil?
+ end
+
+ def instantiator?
+ @finder == :find_initial && !@instantiator.nil?
+ end
+
+ def bang?
+ @bang
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/locale/en-US.rb b/activerecord/lib/active_record/locale/en-US.rb
deleted file mode 100644
index b31e13ed3a..0000000000
--- a/activerecord/lib/active_record/locale/en-US.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-I18n.backend.store_translations :'en-US', {
- :active_record => {
- :error_messages => {
- :inclusion => "is not included in the list",
- :exclusion => "is reserved",
- :invalid => "is invalid",
- :confirmation => "doesn't match confirmation",
- :accepted => "must be accepted",
- :empty => "can't be empty",
- :blank => "can't be blank",
- :too_long => "is too long (maximum is {{count}} characters)",
- :too_short => "is too short (minimum is {{count}} characters)",
- :wrong_length => "is the wrong length (should be {{count}} characters)",
- :taken => "has already been taken",
- :not_a_number => "is not a number",
- :greater_than => "must be greater than {{count}}",
- :greater_than_or_equal_to => "must be greater than or equal to {{count}}",
- :equal_to => "must be equal to {{count}}",
- :less_than => "must be less than {{count}}",
- :less_than_or_equal_to => "must be less than or equal to {{count}}",
- :odd => "must be odd",
- :even => "must be even"
- }
- }
-} \ No newline at end of file
diff --git a/activerecord/lib/active_record/locale/en-US.yml b/activerecord/lib/active_record/locale/en-US.yml
new file mode 100644
index 0000000000..8148f31a81
--- /dev/null
+++ b/activerecord/lib/active_record/locale/en-US.yml
@@ -0,0 +1,33 @@
+en-US:
+ activerecord:
+ errors:
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "is not included in the list"
+ exclusion: "is reserved"
+ invalid: "is invalid"
+ confirmation: "doesn't match confirmation"
+ accepted: "must be accepted"
+ empty: "can't be empty"
+ blank: "can't be blank"
+ too_long: "is too long (maximum is {{count}} characters)"
+ too_short: "is too short (minimum is {{count}} characters)"
+ wrong_length: "is the wrong length (should be {{count}} characters)"
+ taken: "has already been taken"
+ not_a_number: "is not a number"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ # Append your own errors here or at the model/attributes scope.
+
+ models:
+ # Overrides default messages
+
+ attributes:
+ # Overrides model and default messages.
+
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index fd77f27b77..1d843fff28 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -406,11 +406,17 @@ module ActiveRecord
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
end
+ def get_all_versions
+ Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
+ end
+
def current_version
- version = Base.connection.select_values(
- "SELECT version FROM #{schema_migrations_table_name}"
- ).map(&:to_i).max rescue nil
- version || 0
+ sm_table = schema_migrations_table_name
+ if Base.connection.table_exists?(sm_table)
+ get_all_versions.max || 0
+ else
+ 0
+ end
end
def proper_table_name(name)
@@ -426,7 +432,7 @@ module ActiveRecord
end
def current_version
- self.class.current_version
+ migrated.last || 0
end
def current_migration
@@ -461,14 +467,22 @@ module ActiveRecord
Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
# On our way up, we skip migrating the ones we've already migrated
- # On our way down, we skip reverting the ones we've never migrated
next if up? && migrated.include?(migration.version.to_i)
+ # On our way down, we skip reverting the ones we've never migrated
if down? && !migrated.include?(migration.version.to_i)
migration.announce 'never migrated, skipping'; migration.write
- else
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
+ next
+ end
+
+ begin
+ ddl_transaction do
+ migration.migrate(@direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ rescue => e
+ canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
+ raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
end
@@ -509,17 +523,19 @@ module ActiveRecord
end
def migrated
- sm_table = self.class.schema_migrations_table_name
- Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
+ @migrated_versions ||= self.class.get_all_versions
end
private
def record_version_state_after_migrating(version)
sm_table = self.class.schema_migrations_table_name
+ @migrated_versions ||= []
if down?
+ @migrated_versions.delete(version.to_i)
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
else
+ @migrated_versions.push(version.to_i).sort!
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
end
end
@@ -531,5 +547,14 @@ module ActiveRecord
def down?
@direction == :down
end
+
+ # Wrap the migration in a transaction only if supported by the adapter.
+ def ddl_transaction(&block)
+ if Base.connection.supports_ddl_transactions?
+ Base.transaction { block.call }
+ else
+ block.call
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index eb887ee550..46eaa42a55 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -103,7 +103,7 @@ module ActiveRecord
attr_reader :proxy_scope, :proxy_options
[].methods.each do |m|
- unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/
delegate m, :to => :proxy_found
end
end
@@ -140,8 +140,8 @@ module ActiveRecord
@found ? @found.empty? : count.zero?
end
- def respond_to?(method)
- super || @proxy_scope.respond_to?(method)
+ def respond_to?(method, include_private = false)
+ super || @proxy_scope.respond_to?(method, include_private)
end
def any?
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3f74c03714..935b1939d8 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -109,7 +109,7 @@ module ActiveRecord
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
- name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
end
def sanitized_conditions #:nodoc:
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index ca5591ae35..ffaa41282f 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -37,7 +37,7 @@ module ActiveRecord
$queries_executed = []
yield
ensure
- assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
+ assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end
def assert_no_queries(&block)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index d1bf26f331..970da701c7 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -91,11 +91,11 @@ module ActiveRecord
end
def destroy_with_transactions #:nodoc:
- transaction { destroy_without_transactions }
+ with_transaction_returning_status(:destroy_without_transactions)
end
def save_with_transactions(perform_validation = true) #:nodoc:
- rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
+ rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
end
def save_with_transactions! #:nodoc:
@@ -118,5 +118,17 @@ module ActiveRecord
end
raise
end
+
+ # Executes +method+ within a transaction and captures its return value as a
+ # status flag. If the status is true the transaction is committed, otherwise
+ # a ROLLBACK is issued. In any case the status flag is returned.
+ def with_transaction_returning_status(method, *args)
+ status = nil
+ transaction do
+ status = send(method, *args)
+ raise ActiveRecord::Rollback unless status
+ end
+ status
+ end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index b8b695e529..687f1261cd 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -21,8 +21,8 @@ module ActiveRecord
class << self
def default_error_messages
- ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('active_record.error_messages').")
- I18n.translate 'active_record.error_messages'
+ ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').")
+ I18n.translate 'activerecord.errors.messages'
end
end
@@ -38,22 +38,24 @@ module ActiveRecord
add(:base, msg)
end
- # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
+ # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
- # If no +msg+ is supplied, "invalid" is assumed.
- def add(attribute, message = nil)
- message ||= I18n.translate :"active_record.error_messages.invalid"
+ # If no +messsage+ is supplied, :invalid is assumed.
+ # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
+ def add(attribute, message = nil, options = {})
+ message ||= :invalid
+ message = generate_message(attribute, message, options) if message.is_a?(Symbol)
@errors[attribute.to_s] ||= []
@errors[attribute.to_s] << message
- end
+ end
# Will add an error message to each of the attributes in +attributes+ that is empty.
def add_on_empty(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
is_empty = value.respond_to?("empty?") ? value.empty? : false
- add(attr, generate_message(attr, :empty, :default => custom_message)) unless !value.nil? && !is_empty
+ add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty
end
end
@@ -61,16 +63,50 @@ module ActiveRecord
def add_on_blank(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
- add(attr, generate_message(attr, :blank, :default => custom_message)) if value.blank?
+ add(attr, :blank, :default => custom_message) if value.blank?
end
end
- def generate_message(attr, key, options = {})
- msgs = base_classes(@base.class).map{|klass| :"custom.#{klass.name.underscore}.#{attr}.#{key}"}
- msgs << options[:default] if options[:default]
- msgs << key
+ # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
+ # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
+ # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
+ # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
+ # translated attribute name and the value are available for interpolation.
+ #
+ # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
+ # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
+ # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
+ #
+ # <ol>
+ # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
+ # <li><tt>activerecord.errors.models.admin.blank</tt></li>
+ # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
+ # <li><tt>activerecord.errors.models.user.blank</tt></li>
+ # <li><tt>activerecord.errors.messages.blank</tt></li>
+ # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
+ # </ol>
+ def generate_message(attribute, message = :invalid, options = {})
+
+ defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
+ [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
+ :"models.#{klass.name.underscore}.#{message}" ]
+ end
+
+ defaults << options.delete(:default)
+ defaults = defaults.compact.flatten << :"messages.#{message}"
- I18n.t nil, options.merge(:default => msgs, :scope => [:active_record, :error_messages])
+ model_name = @base.class.name
+ key = defaults.shift
+ value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
+
+ options = { :default => defaults,
+ :model => @base.class.human_name,
+ :attribute => @base.class.human_attribute_name(attribute.to_s),
+ :value => value,
+ :scope => [:activerecord, :errors]
+ }.merge(options)
+
+ I18n.translate(key, options)
end
# Returns true if the specified +attribute+ has errors associated with it.
@@ -166,9 +202,9 @@ module ActiveRecord
if attr == "base"
full_messages << message
else
- key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}"
- attr_name = I18n.translate(key, :locale => options[:locale], :default => @base.class.human_attribute_name(attr))
- full_messages << attr_name + " " + message
+ #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
+ attr_name = @base.class.human_attribute_name(attr)
+ full_messages << attr_name + ' ' + message
end
end
end
@@ -219,16 +255,6 @@ module ActiveRecord
end
end
- protected
-
- # TODO maybe this should be on ActiveRecord::Base, maybe #self_and_descendents_from_active_record
- def base_classes(klass)
- classes = [klass]
- while klass != klass.base_class
- classes << klass = klass.superclass
- end
- classes
- end
end
@@ -398,8 +424,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
- message = record.errors.generate_message(attr_name, :confirmation, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :confirmation, :default => configuration[:message])
end
end
end
@@ -441,8 +466,7 @@ module ActiveRecord
validates_each(attr_names,configuration) do |record, attr_name, value|
unless value == configuration[:accept]
- message = record.errors.generate_message(attr_name, :accepted, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :accepted, :default => configuration[:message])
end
end
end
@@ -544,11 +568,9 @@ module ActiveRecord
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
- message = record.errors.generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
- record.errors.add(attr, message)
+ record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
- message = record.errors.generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
- record.errors.add(attr, message)
+ record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@@ -563,8 +585,7 @@ module ActiveRecord
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
key = message_options[option]
custom_message = options[:message] || options[key]
- message = record.errors.generate_message(attr, key, :default => custom_message, :count => option_value)
- record.errors.add(attr, message)
+ record.errors.add(attr, key, :default => custom_message, :count => option_value)
end
end
end
@@ -629,12 +650,11 @@ module ActiveRecord
if value.nil?
comparison_operator = "IS ?"
- else
+ elsif is_text_column
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
-
- if is_text_column
- value = value.to_s
- end
+ value = value.to_s
+ else
+ comparison_operator = "= ?"
end
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
@@ -662,8 +682,7 @@ module ActiveRecord
finder_class.with_exclusive_scope do
if finder_class.exists?([condition_sql, *condition_params])
- message = record.errors.generate_message(attr_name, :taken, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
end
end
end
@@ -701,8 +720,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless value.to_s =~ configuration[:with]
- message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
end
@@ -736,8 +754,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless enum.include?(value)
- message = record.errors.generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
end
end
end
@@ -771,8 +788,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
if enum.include?(value)
- message = record.errors.generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
end
end
end
@@ -814,8 +830,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
- message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
end
@@ -864,8 +879,7 @@ module ActiveRecord
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
- message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
raw_value = raw_value.to_i
@@ -873,8 +887,7 @@ module ActiveRecord
begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
- message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
end
@@ -883,12 +896,10 @@ module ActiveRecord
case option
when :odd, :even
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
- message = record.errors.generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
end
else
- message = record.errors.generate_message(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option])
- record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
+ record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
end
end
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 1f8a1090eb..8c9ae8a031 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -68,6 +68,18 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
end
+ def test_eager_association_loading_with_has_many_sti_and_subclasses
+ silly = SillyReply.new(:title => "gaga", :content => "boo-boo", :parent_id => 1)
+ silly.parent_id = 1
+ assert silly.save
+
+ topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id')
+ assert_no_queries do
+ assert_equal 2, topics[0].replies.size
+ assert_equal 0, topics[1].replies.size
+ end
+ end
+
def test_eager_association_loading_with_belongs_to_sti
replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
assert replies.include?(topics(:second))
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index 80cfc84b32..12dec5ccd1 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -1,5 +1,20 @@
require 'cases/helper'
+module Remembered
+ def self.included(base)
+ base.extend ClassMethods
+ base.class_eval do
+ after_create :remember
+ protected
+ def remember; self.class.remembered << self; end
+ end
+ end
+
+ module ClassMethods
+ def remembered; @@remembered ||= []; end
+ def rand; @@remembered.rand; end
+ end
+end
class ShapeExpression < ActiveRecord::Base
belongs_to :shape, :polymorphic => true
@@ -8,26 +23,33 @@ end
class Circle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class Square < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class Triangle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class PaintColor < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
+ include Remembered
end
class PaintTexture < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
+ include Remembered
end
class NonPolyOne < ActiveRecord::Base
has_many :paint_colors
+ include Remembered
end
class NonPolyTwo < ActiveRecord::Base
has_many :paint_textures
+ include Remembered
end
@@ -49,23 +71,19 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
end
- # meant to be supplied as an ID, never returns 0
- def rand_simple
- val = (NUM_SIMPLE_OBJS * rand).round
- val == 0 ? 1 : val
- end
-
def generate_test_object_graphs
1.upto(NUM_SIMPLE_OBJS) do
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
end
- 1.upto(NUM_SIMPLE_OBJS) do |i|
- PaintColor.create!(:non_poly_one_id => rand_simple)
- PaintTexture.create!(:non_poly_two_id => rand_simple)
+ 1.upto(NUM_SIMPLE_OBJS) do
+ PaintColor.create!(:non_poly_one_id => NonPolyOne.rand.id)
+ PaintTexture.create!(:non_poly_two_id => NonPolyTwo.rand.id)
end
- 1.upto(NUM_SHAPE_EXPRESSIONS) do |i|
- ShapeExpression.create!(:shape_type => [Circle, Square, Triangle].rand.to_s, :shape_id => rand_simple,
- :paint_type => [PaintColor, PaintTexture].rand.to_s, :paint_id => rand_simple)
+ 1.upto(NUM_SHAPE_EXPRESSIONS) do
+ shape_type = [Circle, Square, Triangle].rand
+ paint_type = [PaintColor, PaintTexture].rand
+ ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.rand.id,
+ :paint_type => paint_type.to_s, :paint_id => paint_type.rand.id)
end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 58506574f8..e78624a98d 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -38,6 +38,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_loading_with_one_association_with_non_preload
+ posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC')
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+
def test_loading_conditions_with_or
posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
@@ -254,9 +260,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_through
- posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
- posts_with_author = people(:michael).posts.find(:all, :include => :author )
- posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
+ posts_with_comments = people(:michael).posts.find(:all, :include => :comments, :order => 'posts.id')
+ posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id')
+ posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id')
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
@@ -559,6 +565,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nothing_raised { Post.find(:all, :include => 'comments') }
end
+ def test_eager_with_floating_point_numbers
+ assert_queries(2) do
+ # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
+ Comment.find :all, :conditions => "123.456 = 123.456", :include => :post
+ end
+ end
+
def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts
assert_no_queries {assert_equal 5, author.posts.size}
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index f71b122ff0..0572418e3b 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -450,6 +450,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
end
+ def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
+ # interpolate once:
+ assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
+ # interpolate again, for a different project id
+ assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
+ end
+
def test_find_in_association_with_custom_finder_sql_and_string_id
assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
end
@@ -640,8 +647,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save
developer.reload
assert_equal 2, developer.projects.length
- assert_equal projects(:active_record), developer.projects[0]
- assert_equal projects(:action_controller), developer.projects[1]
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_assign_ids_ignoring_blanks
@@ -650,8 +656,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save
developer.reload
assert_equal 2, developer.projects.length
- assert_equal projects(:active_record), developer.projects[0]
- assert_equal projects(:action_controller), developer.projects[1]
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_select_limited_ids_list
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index b806e885e1..17fd88ce65 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -48,6 +48,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, Firm.find(:first).plain_clients.count(:name)
end
+ def test_counting_with_association_limit
+ firm = companies(:first_firm)
+ assert_equal firm.limited_clients.length, firm.limited_clients.size
+ assert_equal firm.limited_clients.length, firm.limited_clients.count
+ end
+
def test_finding
assert_equal 2, Firm.find(:first).clients.length
end
@@ -395,6 +401,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, company.clients_of_firm.size
end
+ def test_collection_size_twice_for_regressions
+ post = posts(:thinking)
+ assert_equal 0, post.readers.size
+ # This test needs a post that has no readers, we assert it to ensure it holds,
+ # but need to reload the post because the very call to #size hides the bug.
+ post.reload
+ post.readers.build
+ size1 = post.readers.size
+ size2 = post.readers.size
+ assert_equal size1, size2
+ end
+
def test_build_many
company = companies(:first_firm)
new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index d51a3c7e1c..353262c81b 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -196,4 +196,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# due to Unknown column 'comments.id'
assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title('Welcome to the weblog')
end
+
+ def test_count_with_include_should_alias_join_table
+ assert_equal 2, people(:michael).posts.count(:include => :readers)
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 99639849a5..ec06be5eba 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -79,6 +79,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
end
+ def test_natural_assignment_to_already_associated_record
+ company = companies(:first_firm)
+ account = accounts(:signals37)
+ assert_equal company.account, account
+ company.account = account
+ company.reload
+ account.reload
+ assert_equal company.account, account
+ end
+
def test_assignment_without_replacement
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 3eb66bc941..77e3cb1776 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -44,19 +44,23 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_polymorphic
assert_equal clubs(:moustache_club), @member.sponsor_club
end
-
+
def has_one_through_to_has_many
assert_equal 2, @member.fellow_members.size
end
-
+
def test_has_one_through_eager_loading
- members = Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
+ members = assert_queries(3) do #base table, through table, clubs table
+ Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
+ end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].club}
end
-
+
def test_has_one_through_eager_loading_through_polymorphic
- members = Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
+ members = assert_queries(3) do #base table, through table, clubs table
+ Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
+ end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].sponsor_club}
end
@@ -71,4 +75,39 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries {clubs[0].sponsored_member}
end
+ def test_has_one_through_nonpreload_eagerloading
+ members = assert_queries(1) do
+ Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries {members[0].club}
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic
+ members = assert_queries(1) do
+ Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries {members[0].sponsor_club}
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
+ Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save!
+ members = assert_queries(1) do
+ Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
+ assert_equal clubs(:crazy_club), members[0].sponsor_club
+ end
+
+ def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
+ assert_nil Member.new.club
+ end
+
+ def test_assigning_association_correctly_assigns_target
+ new_member = Member.create(:name => "Chris")
+ new_member.club = new_club = Club.create(:name => "LRUG")
+ assert_equal new_club, new_member.club.target
+ end
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 9e79d9c8a1..7a0427aabc 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -694,6 +694,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert ! david.categories.include?(category)
end
+ def test_has_many_through_goes_through_all_sti_classes
+ sub_sti_post = SubStiPost.create!(:title => 'test', :body => 'test', :author_id => 1)
+ new_comment = sub_sti_post.comments.create(:body => 'test')
+
+ assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
+ end
+
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 4904feeb7d..0b2731ecd7 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -27,7 +27,7 @@ require 'models/sponsor'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
- :computers
+ :computers, :people, :readers
def test_include_with_order_works
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
@@ -45,7 +45,7 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal [], person.readers.find(:all)
person.save!
reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
- assert_equal [reader], person.readers.find(:all)
+ assert person.readers.find(reader.id)
end
def test_force_reload
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 36d30ade5e..c8111358e3 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -138,7 +138,7 @@ class BasicsTest < ActiveRecord::TestCase
if current_adapter?(:MysqlAdapter)
def test_read_attributes_before_type_cast_on_boolean
bool = Booleantest.create({ "value" => false })
- assert_equal 0, bool.attributes_before_type_cast["value"]
+ assert_equal "0", bool.reload.attributes_before_type_cast["value"]
end
end
@@ -616,7 +616,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_update_counter
- category = Category.first
+ category = categories(:general)
assert_nil category.categorizations_count
assert_equal 2, category.categorizations.count
@@ -1114,11 +1114,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_boolean
+ b_nil = Booleantest.create({ "value" => nil })
+ nil_id = b_nil.id
b_false = Booleantest.create({ "value" => false })
false_id = b_false.id
b_true = Booleantest.create({ "value" => true })
true_id = b_true.id
+ b_nil = Booleantest.find(nil_id)
+ assert_nil b_nil.value
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
@@ -1126,11 +1130,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_boolean_cast_from_string
+ b_blank = Booleantest.create({ "value" => "" })
+ blank_id = b_blank.id
b_false = Booleantest.create({ "value" => "0" })
false_id = b_false.id
b_true = Booleantest.create({ "value" => "1" })
true_id = b_true.id
+ b_blank = Booleantest.find(blank_id)
+ assert_nil b_blank.value
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 540f42f4b6..98abc8eac8 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -9,13 +9,13 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
end
# Avoid column definitions in create table statements like:
- # `title` varchar(255) DEFAULT NULL NULL
+ # `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
column = ActiveRecord::ConnectionAdapters::Column.new("title", nil, "varchar(20)")
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal "title varchar(20) NULL", column_def.to_sql
+ assert_equal "title varchar(20)", column_def.to_sql
end
def test_should_include_default_clause_when_default_is_present
@@ -23,7 +23,7 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NULL}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql
end
def test_should_specify_not_null_if_null_option_is_false
@@ -33,4 +33,4 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 2ea85417da..3473b846a0 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -5,7 +5,7 @@ require 'models/entrant'
class DefaultTest < ActiveRecord::TestCase
def test_nil_defaults_for_not_null_columns
column_defaults =
- if current_adapter?(:MysqlAdapter) && Mysql.client_version < 50051
+ if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version))
{ 'id' => nil, 'name' => '', 'course_id' => nil }
else
{ 'id' => nil, 'name' => nil, 'course_id' => nil }
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index feb47a15a8..4fe1d79f4d 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -191,6 +191,42 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.changed?
end
+ def test_reverted_changes_are_not_dirty
+ phrase = "shiver me timbers"
+ pirate = Pirate.create!(:catchphrase => phrase)
+ pirate.catchphrase = "*hic*"
+ assert pirate.changed?
+ pirate.catchphrase = phrase
+ assert !pirate.changed?
+ end
+
+ def test_reverted_changes_are_not_dirty_after_multiple_changes
+ phrase = "shiver me timbers"
+ pirate = Pirate.create!(:catchphrase => phrase)
+ 10.times do |i|
+ pirate.catchphrase = "*hic*" * i
+ assert pirate.changed?
+ end
+ assert pirate.changed?
+ pirate.catchphrase = phrase
+ assert !pirate.changed?
+ end
+
+
+ def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back
+ pirate = Pirate.create!(:catchphrase => "Yar!")
+
+ pirate.parrot_id = 1
+ assert pirate.changed?
+ assert pirate.parrot_id_changed?
+ assert !pirate.catchphrase_changed?
+
+ pirate.parrot_id = nil
+ assert !pirate.changed?
+ assert !pirate.parrot_id_changed?
+ assert !pirate.catchphrase_changed?
+ end
+
def test_save_should_store_serialized_attributes_even_with_partial_updates
with_partial_updates(Topic) do
topic = Topic.create!(:content => {:a => "a"})
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index b97db73b68..2ce49ed76f 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -12,6 +12,57 @@ require 'models/customer'
require 'models/job'
require 'models/categorization'
+class DynamicFinderMatchTest < ActiveRecord::TestCase
+ def test_find_no_match
+ assert_nil ActiveRecord::DynamicFinderMatch.match("not_a_finder")
+ end
+
+ def test_find_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.finder?
+ assert_equal :find_initial, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def find_by_bang
+ match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location!")
+ assert_not_nil match
+ assert match.finder?
+ assert match.bang?
+ assert_equal :find_initial, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_all_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_all_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.finder?
+ assert_equal :find_every, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_or_initialize_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert !match.finder?
+ assert match.instantiator?
+ assert_equal :find_initial, match.finder
+ assert_equal :new, match.instantiator
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_or_create_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert !match.finder?
+ assert match.instantiator?
+ assert_equal :find_initial, match.finder
+ assert_equal :create, match.instantiator
+ assert_equal %w(age sex location), match.attribute_names
+ end
+end
+
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers
@@ -440,6 +491,11 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_by_title("The First Topic!")
end
+ def test_find_by_one_attribute_bang
+ assert_equal topics(:first), Topic.find_by_title!("The First Topic")
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
+ end
+
def test_find_by_one_attribute_caches_dynamic_finder
# ensure this test can run independently of order
class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 0530ba9bd9..f30d58546e 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,4 +1,5 @@
$:.unshift(File.dirname(__FILE__) + '/../../lib')
+$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
require 'config'
require 'test/unit'
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
new file mode 100644
index 0000000000..ea06e377e3
--- /dev/null
+++ b/activerecord/test/cases/i18n_test.rb
@@ -0,0 +1,41 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class ActiveRecordI18nTests < Test::Unit::TestCase
+
+ def setup
+ I18n.backend = I18n::Backend::Simple.new
+ end
+
+ def test_translated_model_attributes
+ I18n.backend.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Topic.human_attribute_name('title')
+ end
+
+ def test_translated_model_attributes_with_sti
+ I18n.backend.store_translations 'en-US', :activerecord => {:attributes => {:reply => {:title => 'reply title attribute'} } }
+ assert_equal 'reply title attribute', Reply.human_attribute_name('title')
+ end
+
+ def test_translated_model_attributes_with_sti_fallback
+ I18n.backend.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Reply.human_attribute_name('title')
+ end
+
+ def test_translated_model_names
+ I18n.backend.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Topic.human_name
+ end
+
+ def test_translated_model_names_with_sti
+ I18n.backend.store_translations 'en-US', :activerecord => {:models => {:reply => 'reply model'} }
+ assert_equal 'reply model', Reply.human_name
+ end
+
+ def test_translated_model_names_with_sti_fallback
+ I18n.backend.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Reply.human_name
+ end
+end
+
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 920f719995..c1a8da2270 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -237,6 +237,39 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ def test_create_table_with_timestamps_should_create_datetime_columns
+ table_name = :testings
+
+ Person.connection.create_table table_name do |t|
+ t.timestamps
+ end
+ created_columns = Person.connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert created_at_column.null
+ assert updated_at_column.null
+ ensure
+ Person.connection.drop_table table_name rescue nil
+ end
+
+ def test_create_table_with_timestamps_should_create_datetime_columns_with_options
+ table_name = :testings
+
+ Person.connection.create_table table_name do |t|
+ t.timestamps :null => false
+ end
+ created_columns = Person.connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert !created_at_column.null
+ assert !updated_at_column.null
+ ensure
+ Person.connection.drop_table table_name rescue nil
+ end
# SQL Server, Sybase, and SQLite3 will not allow you to add a NOT NULL
# column to a table without a default value.
@@ -409,10 +442,7 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::Migration.add_column :people, :intelligence_quotient, :tinyint
Person.reset_column_information
- Person.create :intelligence_quotient => 300
- jonnyg = Person.find(:first)
- assert_equal 127, jonnyg.intelligence_quotient
- jonnyg.destroy
+ assert_match /tinyint/, Person.columns_hash['intelligence_quotient'].sql_type
ensure
ActiveRecord::Migration.remove_column :people, :intelligence_quotient rescue nil
end
@@ -904,6 +934,21 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal(0, ActiveRecord::Migrator.current_version)
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_migrator_one_up_with_exception_and_rollback
+ assert !Person.column_methods_hash.include?(:last_name)
+
+ e = assert_raises(StandardError) do
+ ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
+ end
+
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ end
+ end
+
def test_finds_migrations
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
[['1', 'people_have_last_names'],
@@ -1192,8 +1237,8 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_timestamps_creates_updated_at_and_created_at
with_new_table do |t|
- t.expects(:column).with(:created_at, :datetime)
- t.expects(:column).with(:updated_at, :datetime)
+ t.expects(:column).with(:created_at, :datetime, kind_of(Hash))
+ t.expects(:column).with(:updated_at, :datetime, kind_of(Hash))
t.timestamps
end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index bd6ec23853..6f6ea1cbe9 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -4,6 +4,7 @@ require 'models/topic'
require 'models/comment'
require 'models/reply'
require 'models/author'
+require 'models/developer'
class NamedScopeTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
@@ -51,6 +52,11 @@ class NamedScopeTest < ActiveRecord::TestCase
assert Topic.approved.respond_to?(:length)
end
+ def test_respond_to_respects_include_private_parameter
+ assert !Topic.approved.respond_to?(:load_found)
+ assert Topic.approved.respond_to?(:load_found, true)
+ end
+
def test_subclasses_inherit_scopes
assert Topic.scopes.include?(:base)
@@ -238,4 +244,12 @@ class NamedScopeTest < ActiveRecord::TestCase
assert topic.approved
assert_equal 'lifo', topic.author_name
end
+
+ def test_find_all_should_behave_like_select
+ assert_equal Topic.base.select(&:approved), Topic.base.find_all(&:approved)
+ end
+
+ def test_should_use_where_in_query_for_named_scope
+ assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises)
+ end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index eae2104531..171d0e6dae 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -116,8 +116,9 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_cache_is_expired_by_habtm_delete
ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
ActiveRecord::Base.cache do
- c = Category.find(:first)
- p = Post.find(:first)
+ c = Category.find(1)
+ p = Post.find(1)
+ assert p.categories.any?
p.categories.delete_all
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 723062e3b8..4b86e32dbf 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -166,6 +166,10 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end
+ def test_reflection_should_not_raise_error_when_compared_to_other_object
+ assert_nothing_raised { Firm.reflections[:clients] == Object.new }
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 06a76eacc3..af3ee6ddba 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/book'
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -86,8 +87,7 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.find(2).approved?, "Second should still be approved"
end
-
- def test_callback_rollback_in_save
+ def test_raising_exception_in_callback_rollbacks_in_save
add_exception_raising_after_save_callback_to_topic
begin
@@ -102,6 +102,54 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_cancellation_from_before_destroy_rollbacks_in_destroy
+ add_cancelling_before_destroy_with_db_side_effect_to_topic
+ begin
+ nbooks_before_destroy = Book.count
+ status = @first.destroy
+ assert !status
+ assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
+ assert_equal nbooks_before_destroy, Book.count
+ ensure
+ remove_cancelling_before_destroy_with_db_side_effect_to_topic
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ status = @first.save
+ assert !status
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save!
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ @first.save!
+ flunk
+ rescue => e
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
def test_callback_rollback_in_create
new_topic = Topic.new(
:title => "A new topic",
@@ -221,6 +269,16 @@ class TransactionTest < ActiveRecord::TestCase
def remove_exception_raising_after_create_callback_to_topic
Topic.class_eval { remove_method :after_create }
end
+
+ %w(validation save destroy).each do |filter|
+ define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "def before_#{filter}() Book.create; false end"
+ end
+
+ define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "remove_method :before_#{filter}"
+ end
+ end
end
if current_adapter?(:PostgreSQLAdapter)
diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb
index 86834fe920..43592bcee3 100644
--- a/activerecord/test/cases/validations_i18n_test.rb
+++ b/activerecord/test/cases/validations_i18n_test.rb
@@ -6,18 +6,18 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
def setup
reset_callbacks Topic
@topic = Topic.new
- I18n.backend.store_translations('en-US', :active_record => {:error_messages => {:custom => nil}})
+ I18n.backend.store_translations('en-US', :activerecord => {:errors => {:messages => {:custom => nil}}})
end
-
+
def teardown
reset_callbacks Topic
- load 'active_record/locale/en-US.rb'
+ I18n.load_translations File.dirname(__FILE__) + '/../../lib/active_record/locale/en-US.yml'
end
-
+
def unique_topic
@unique ||= Topic.create :title => 'unique!'
end
-
+
def replied_topic
@replied_topic ||= begin
topic = Topic.create(:title => "topic")
@@ -25,7 +25,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
topic
end
end
-
+
def reset_callbacks(*models)
models.each do |model|
model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
@@ -33,28 +33,83 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
end
end
-
+
def test_default_error_messages_is_deprecated
assert_deprecated('ActiveRecord::Errors.default_error_messages') do
ActiveRecord::Errors.default_error_messages
end
end
-
+
# ActiveRecord::Errors
uses_mocha 'ActiveRecord::Errors' do
+
def test_errors_generate_message_translates_custom_model_attribute_key
- global_scope = [:active_record, :error_messages]
- custom_scope = global_scope + [:custom, 'topic', :title]
- I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid]
- @topic.errors.generate_message :title, :invalid, :default => 'default from class def'
+ I18n.expects(:translate).with(
+ :topic,
+ { :count => 1,
+ :default => ['Topic'],
+ :scope => [:activerecord, :models]
+ }
+ ).returns('Topic')
+
+ I18n.expects(:translate).with(
+ :"topic.title",
+ { :count => 1,
+ :default => ['Title'],
+ :scope => [:activerecord, :attributes]
+ }
+ ).returns('Title')
+
+ I18n.expects(:translate).with(
+ :"models.topic.attributes.title.invalid",
+ :value => nil,
+ :scope => [:activerecord, :errors],
+ :default => [
+ :"models.topic.invalid",
+ 'default from class def error 1',
+ :"messages.invalid"],
+ :attribute => "Title",
+ :model => "Topic"
+ ).returns('default from class def error 1')
+
+ @topic.errors.generate_message :title, :invalid, :default => 'default from class def error 1'
end
def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti
- custom_scope = [:active_record, :error_messages, :custom, 'topic', :title]
- I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid]
+ I18n.expects(:translate).with(
+ :reply,
+ { :count => 1,
+ :default => [:topic, 'Reply'],
+ :scope => [:activerecord, :models]
+ }
+ ).returns('Reply')
+
+ I18n.expects(:translate).with(
+ :"reply.title",
+ { :count => 1,
+ :default => [:'topic.title', 'Title'],
+ :scope => [:activerecord, :attributes]
+ }
+ ).returns('Title')
+
+ I18n.expects(:translate).with(
+ :"models.reply.attributes.title.invalid",
+ :value => nil,
+ :scope => [:activerecord, :errors],
+ :default => [
+ :"models.reply.invalid",
+ :"models.topic.attributes.title.invalid",
+ :"models.topic.invalid",
+ 'default from class def',
+ :"messages.invalid"],
+ :model => 'Reply',
+ :attribute => 'Title'
+ ).returns("default from class def")
+
Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def'
+
end
def test_errors_add_on_empty_generates_message
@@ -78,12 +133,12 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
end
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
- @topic.errors.instance_variable_set :@errors, { 'title' => 'empty' }
- I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", :locale => 'en-US', :default => 'Title').returns('Title')
+ @topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] }
+ I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title')
@topic.errors.full_messages :locale => 'en-US'
end
- end
-
+ end
+
# ActiveRecord::Validations
uses_mocha 'ActiveRecord::Validations' do
# validates_confirmation_of w/ mocha
@@ -101,7 +156,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'})
@topic.valid?
end
-
+
# validates_acceptance_of w/ mocha
def test_validates_acceptance_of_generates_message
@@ -115,9 +170,9 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'})
@topic.valid?
end
-
+
# validates_presence_of w/ mocha
-
+
def test_validates_presence_of_generates_message
Topic.validates_presence_of :title
@topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
@@ -129,7 +184,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
@topic.valid?
end
-
+
def test_validates_length_of_within_generates_message_with_title_too_short
Topic.validates_length_of :title, :within => 3..5
@topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
@@ -183,7 +238,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
@topic.valid?
end
-
+
# validates_length_of :is w/ mocha
def test_validates_length_of_is_generates_message
@@ -197,23 +252,23 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'})
@topic.valid?
end
-
+
# validates_uniqueness_of w/ mocha
def test_validates_uniqueness_of_generates_message
Topic.validates_uniqueness_of :title
@topic.title = unique_topic.title
- @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil})
+ @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'})
@topic.valid?
end
def test_validates_uniqueness_of_generates_message_with_custom_default_message
Topic.validates_uniqueness_of :title, :message => 'custom'
@topic.title = unique_topic.title
- @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom'})
+ @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'})
@topic.valid?
end
-
+
# validates_format_of w/ mocha
def test_validates_format_of_generates_message
@@ -229,7 +284,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'})
@topic.valid?
end
-
+
# validates_inclusion_of w/ mocha
def test_validates_inclusion_of_generates_message
@@ -245,7 +300,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'})
@topic.valid?
end
-
+
# validates_exclusion_of w/ mocha
def test_validates_exclusion_of_generates_message
@@ -261,7 +316,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'})
@topic.valid?
end
-
+
# validates_numericality_of without :only_integer w/ mocha
def test_validates_numericality_of_generates_message
@@ -277,7 +332,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
@topic.valid?
end
-
+
# validates_numericality_of with :only_integer w/ mocha
def test_validates_numericality_of_only_integer_generates_message
@@ -293,7 +348,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
@topic.valid?
end
-
+
# validates_numericality_of :odd w/ mocha
def test_validates_numericality_of_odd_generates_message
@@ -309,7 +364,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'})
@topic.valid?
end
-
+
# validates_numericality_of :less_than w/ mocha
def test_validates_numericality_of_less_than_generates_message
@@ -325,7 +380,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'})
@topic.valid?
end
-
+
# validates_associated w/ mocha
def test_validates_associated_generates_message
@@ -340,284 +395,464 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
replied_topic.valid?
end
end
-
+
# validates_confirmation_of w/o mocha
-
+
def test_validates_confirmation_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:confirmation => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
+
Topic.validates_confirmation_of :title
@topic.title_confirmation = 'foo'
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_confirmation_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
+
Topic.validates_confirmation_of :title
@topic.title_confirmation = 'foo'
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_acceptance_of w/o mocha
-
+
def test_validates_acceptance_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:accepted => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
+
Topic.validates_acceptance_of :title, :allow_nil => false
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_acceptance_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
+
Topic.validates_acceptance_of :title, :allow_nil => false
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_presence_of w/o mocha
-
+
def test_validates_presence_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:blank => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
+
Topic.validates_presence_of :title
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_presence_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
+
Topic.validates_presence_of :title
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_length_of :within w/o mocha
-
+
def test_validates_length_of_within_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:too_short => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
+
Topic.validates_length_of :title, :within => 3..5
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_length_of_within_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
+
Topic.validates_length_of :title, :within => 3..5
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_length_of :is w/o mocha
-
+
def test_validates_length_of_within_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
Topic.validates_length_of :title, :is => 5
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_length_of_within_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
Topic.validates_length_of :title, :is => 5
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_uniqueness_of w/o mocha
-
+
def test_validates_length_of_within_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
Topic.validates_length_of :title, :is => 5
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_length_of_within_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
Topic.validates_length_of :title, :is => 5
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
-
+
+
# validates_format_of w/o mocha
-
+
def test_validates_format_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:invalid => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_format_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_inclusion_of w/o mocha
-
+
def test_validates_inclusion_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:inclusion => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
+
Topic.validates_inclusion_of :title, :in => %w(a b c)
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_inclusion_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
+
Topic.validates_inclusion_of :title, :in => %w(a b c)
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_exclusion_of w/o mocha
-
+
def test_validates_exclusion_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:exclusion => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
+
Topic.validates_exclusion_of :title, :in => %w(a b c)
@topic.title = 'a'
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_exclusion_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
+
Topic.validates_exclusion_of :title, :in => %w(a b c)
@topic.title = 'a'
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_numericality_of without :only_integer w/o mocha
-
+
def test_validates_numericality_of_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
Topic.validates_numericality_of :title
@topic.title = 'a'
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_numericality_of_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true
@topic.title = 'a'
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_numericality_of with :only_integer w/o mocha
-
+
def test_validates_numericality_of_only_integer_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true
@topic.title = 'a'
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_numericality_of_only_integer_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true
@topic.title = 'a'
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_numericality_of :odd w/o mocha
-
+
def test_validates_numericality_of_odd_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:odd => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true, :odd => true
@topic.title = 0
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_numericality_of_odd_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true, :odd => true
@topic.title = 0
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
+
# validates_numericality_of :less_than w/o mocha
-
+
def test_validates_numericality_of_less_than_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:less_than => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
@topic.title = 1
@topic.valid?
assert_equal 'custom message', @topic.errors.on(:title)
end
-
+
def test_validates_numericality_of_less_than_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
+
Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
@topic.title = 1
@topic.valid?
assert_equal 'global message', @topic.errors.on(:title)
end
-
-
+
+
# validates_associated w/o mocha
-
+
def test_validates_associated_finds_custom_model_key_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
Topic.validates_associated :replies
replied_topic.valid?
assert_equal 'custom message', replied_topic.errors.on(:replies)
end
-
+
def test_validates_associated_finds_global_default_translation
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
Topic.validates_associated :replies
replied_topic.valid?
assert_equal 'global message', replied_topic.errors.on(:replies)
end
-end \ No newline at end of file
+end
+
+class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase
+ def setup
+ reset_callbacks Topic
+ @topic = Topic.new
+ I18n.backend.store_translations :'en-US', {
+ :activerecord => {
+ :errors => {
+ :messages => {
+ :inclusion => "is not included in the list",
+ :exclusion => "is reserved",
+ :invalid => "is invalid",
+ :confirmation => "doesn't match confirmation",
+ :accepted => "must be accepted",
+ :empty => "can't be empty",
+ :blank => "can't be blank",
+ :too_long => "is too long (maximum is {{count}} characters)",
+ :too_short => "is too short (minimum is {{count}} characters)",
+ :wrong_length => "is the wrong length (should be {{count}} characters)",
+ :taken => "has already been taken",
+ :not_a_number => "is not a number",
+ :greater_than => "must be greater than {{count}}",
+ :greater_than_or_equal_to => "must be greater than or equal to {{count}}",
+ :equal_to => "must be equal to {{count}}",
+ :less_than => "must be less than {{count}}",
+ :less_than_or_equal_to => "must be less than or equal to {{count}}",
+ :odd => "must be odd",
+ :even => "must be even"
+ }
+ }
+ }
+ }
+ end
+
+ def reset_callbacks(*models)
+ models.each do |model|
+ model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ end
+ end
+
+ # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_inclusion_with_default_message
+ assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_inclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_exclusion_with_default_message
+ assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_exclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ def test_generate_message_invalid_with_default_message
+ assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_invalid_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message])
+ def test_generate_message_confirmation_with_default_message
+ assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil)
+ end
+
+ def test_generate_message_confirmation_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message')
+ end
+
+ # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message])
+ def test_generate_message_accepted_with_default_message
+ assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil)
+ end
+
+ def test_generate_message_accepted_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message')
+ end
+
+ # add_on_empty: generate_message(attr, :empty, :default => custom_message)
+ def test_generate_message_empty_with_default_message
+ assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil)
+ end
+
+ def test_generate_message_empty_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message')
+ end
+
+ # add_on_blank: generate_message(attr, :blank, :default => custom_message)
+ def test_generate_message_blank_with_default_message
+ assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil)
+ end
+
+ def test_generate_message_blank_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message')
+ end
+
+ # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
+ def test_generate_message_too_long_with_default_message
+ assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_long_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
+ def test_generate_message_too_short_with_default_message
+ assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_short_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value)
+ def test_generate_message_wrong_length_with_default_message
+ assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_wrong_length_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message])
+ def test_generate_message_taken_with_default_message
+ assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_taken_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_not_a_number_with_default_message
+ assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_not_a_number_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_greater_than_with_default_message
+ assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_greater_than_or_equal_to_with_default_message
+ assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_equal_to_with_default_message
+ assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_with_default_message
+ assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_or_equal_to_with_default_message
+ assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_odd_with_default_message
+ assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_even_with_default_message
+ assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10)
+ end
+end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 4b2d28c80b..a40bda2533 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -1420,8 +1420,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
def test_validates_numericality_of_with_nil_allowed
Topic.validates_numericality_of :approved, :allow_nil => true
- invalid!(BLANK + JUNK)
- valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ invalid!(JUNK)
+ valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_integer_only
@@ -1434,8 +1434,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
def test_validates_numericality_of_with_integer_only_and_nil_allowed
Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true
- invalid!(BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
- valid!(NIL + INTEGERS)
+ invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY)
+ valid!(NIL + BLANK + INTEGERS)
end
def test_validates_numericality_with_greater_than
diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb
index 1fab444e58..140e06d631 100644
--- a/activerecord/test/connections/native_mysql/connection.rb
+++ b/activerecord/test/connections/native_mysql/connection.rb
@@ -2,9 +2,7 @@ print "Using native MySQL\n"
require_dependency 'models/course'
require 'logger'
-RAILS_DEFAULT_LOGGER = Logger.new('debug.log')
-RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
-ActiveRecord::Base.logger = RAILS_DEFAULT_LOGGER
+ActiveRecord::Base.logger = Logger.new("debug.log")
# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
diff --git a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
new file mode 100644
index 0000000000..ffb224dad9
--- /dev/null
+++ b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
@@ -0,0 +1,10 @@
+class MigrationThatRaisesException < ActiveRecord::Migration
+ def self.up
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+
+ def self.down
+ remove_column "people", "last_name"
+ end
+end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 136dc39cf3..c6aa0293c2 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -32,6 +32,9 @@ class Author < ActiveRecord::Base
has_many :special_posts
has_many :special_post_comments, :through => :special_posts, :source => :comments
+ has_many :sti_posts, :class_name => 'StiPost'
+ has_many :sti_post_comments, :through => :sti_posts, :source => :comments
+
has_many :special_nonexistant_posts, :class_name => "SpecialPost", :conditions => "posts.body = 'nonexistant'"
has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => "comments.post_id = 0"
has_many :nonexistant_comments, :through => :posts
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 9f26cacdec..c08476f728 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -43,6 +43,8 @@ class Developer < ActiveRecord::Base
has_many :audit_logs
+ named_scope :jamises, :conditions => {:name => 'Jamis'}
+
validates_inclusion_of :salary, :in => 50000..200000
validates_length_of :name, :within => 3..20
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index e1ab89eca5..44c692b5e7 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -7,7 +7,7 @@ class Project < ActiveRecord::Base
has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true
has_and_belongs_to_many :developers_named_david_with_hash_conditions, :class_name => "Developer", :conditions => { :name => 'David' }, :uniq => true
has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0"
- has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id}'
+ has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id'
has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}"
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index 5ae062c97c..f44c33ae67 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -1,5 +1,5 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
+ create_table :binary_fields, :force => true, :options => 'CHARACTER SET latin1' do |t|
t.binary :tiny_blob, :limit => 255
t.binary :normal_blob, :limit => 65535
t.binary :medium_blob, :limit => 16777215
@@ -9,4 +9,4 @@ ActiveRecord::Schema.define do
t.text :medium_text, :limit => 16777215
t.text :long_text, :limit => 2147483647
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 487a08f4ab..ab5c7c520b 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -60,7 +60,7 @@ ActiveRecord::Schema.define do
end
create_table :booleantests, :force => true do |t|
- t.integer :value
+ t.boolean :value
end
create_table :categories, :force => true do |t|