mirror of
https://github.com/github/rails.git
synced 2026-02-16 17:15:29 -05:00
Merge branch 'master' into savepoints
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,8 @@ module ActiveRecord
|
||||
[Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
|
||||
end
|
||||
|
||||
autoload :VERSION, 'active_record/version'
|
||||
|
||||
autoload :ActiveRecordError, 'active_record/base'
|
||||
autoload :ConnectionNotEstablished, 'active_record/base'
|
||||
|
||||
@@ -49,6 +51,7 @@ module ActiveRecord
|
||||
autoload :Callbacks, 'active_record/callbacks'
|
||||
autoload :Dirty, 'active_record/dirty'
|
||||
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
|
||||
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
||||
autoload :Migration, 'active_record/migration'
|
||||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :NamedScope, 'active_record/named_scope'
|
||||
@@ -58,6 +61,7 @@ module ActiveRecord
|
||||
autoload :Schema, 'active_record/schema'
|
||||
autoload :SchemaDumper, 'active_record/schema_dumper'
|
||||
autoload :Serialization, 'active_record/serialization'
|
||||
autoload :SessionStore, 'active_record/session_store'
|
||||
autoload :TestCase, 'active_record/test_case'
|
||||
autoload :Timestamp, 'active_record/timestamp'
|
||||
autoload :Transactions, 'active_record/transactions'
|
||||
|
||||
@@ -43,7 +43,7 @@ module ActiveRecord
|
||||
# loading in a more high-level (application developer-friendly) manner.
|
||||
module ClassMethods
|
||||
protected
|
||||
|
||||
|
||||
# Eager loads the named associations for the given ActiveRecord record(s).
|
||||
#
|
||||
# In this description, 'association name' shall refer to the name passed
|
||||
@@ -94,8 +94,8 @@ module ActiveRecord
|
||||
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
|
||||
preload_associations(records, parent, preload_options)
|
||||
reflection = reflections[parent]
|
||||
parents = records.map {|record| record.send(reflection.name)}.flatten
|
||||
unless parents.empty? || parents.first.nil?
|
||||
parents = records.map {|record| record.send(reflection.name)}.flatten.compact
|
||||
unless parents.empty?
|
||||
parents.first.class.preload_associations(parents, child)
|
||||
end
|
||||
end
|
||||
@@ -113,7 +113,7 @@ module ActiveRecord
|
||||
# unnecessarily
|
||||
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
|
||||
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
|
||||
|
||||
|
||||
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
|
||||
# the following could call 'preload_belongs_to_association',
|
||||
# 'preload_has_many_association', etc.
|
||||
@@ -128,7 +128,7 @@ module ActiveRecord
|
||||
association_proxy.target.push(*[associated_record].flatten)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
|
||||
parent_records.each do |parent_record|
|
||||
parent_record.send("set_#{reflection_name}_target", associated_record)
|
||||
@@ -183,18 +183,19 @@ module ActiveRecord
|
||||
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
|
||||
:order => options[:order])
|
||||
|
||||
associated_records = reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
|
||||
:order => options[:order])
|
||||
end
|
||||
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
|
||||
end
|
||||
|
||||
def preload_has_one_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
id_to_record_map, ids = construct_id_map(records)
|
||||
id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
|
||||
options = reflection.options
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
if options[:through]
|
||||
@@ -204,9 +205,18 @@ module ActiveRecord
|
||||
unless through_records.empty?
|
||||
source = reflection.source_reflection.name
|
||||
through_records.first.class.preload_associations(through_records, source)
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
if through_reflection.macro == :belongs_to
|
||||
rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
|
||||
rev_primary_key = through_reflection.klass.primary_key
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
else
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -219,7 +229,7 @@ module ActiveRecord
|
||||
options = reflection.options
|
||||
|
||||
primary_key_name = reflection.through_reflection_primary_key_name
|
||||
id_to_record_map, ids = construct_id_map(records, primary_key_name)
|
||||
id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
|
||||
records.each {|record| record.send(reflection.name).loaded}
|
||||
|
||||
if options[:through]
|
||||
@@ -239,7 +249,7 @@ module ActiveRecord
|
||||
reflection.primary_key_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def preload_through_records(records, reflection, through_association)
|
||||
through_reflection = reflections[through_association]
|
||||
through_primary_key = through_reflection.primary_key_name
|
||||
@@ -307,6 +317,7 @@ module ActiveRecord
|
||||
|
||||
klasses_and_ids.each do |klass_and_id|
|
||||
klass_name, id_map = *klass_and_id
|
||||
next if id_map.empty?
|
||||
klass = klass_name.constantize
|
||||
|
||||
table_name = klass.quoted_table_name
|
||||
@@ -323,11 +334,13 @@ module ActiveRecord
|
||||
end
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
associated_records = klass.find(:all, :conditions => [conditions, ids],
|
||||
associated_records = klass.with_exclusive_scope do
|
||||
klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:select => options[:select],
|
||||
:joins => options[:joins],
|
||||
:order => options[:order])
|
||||
end
|
||||
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
|
||||
end
|
||||
end
|
||||
@@ -345,13 +358,15 @@ module ActiveRecord
|
||||
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
reflection.klass.find(:all,
|
||||
reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all,
|
||||
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
||||
:include => preload_options[:include] || options[:include],
|
||||
:conditions => [conditions, ids],
|
||||
:joins => options[:joins],
|
||||
:group => preload_options[:group] || options[:group],
|
||||
:order => preload_options[:order] || options[:order])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ module ActiveRecord
|
||||
through_reflection = reflection.through_reflection
|
||||
source_reflection_names = reflection.source_reflection_names
|
||||
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
|
||||
super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :connector => 'or'}?")
|
||||
super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -153,7 +153,7 @@ module ActiveRecord
|
||||
# #others.destroy_all | X | X | X
|
||||
# #others.find(*args) | X | X | X
|
||||
# #others.find_first | X | |
|
||||
# #others.exist? | X | X | X
|
||||
# #others.exists? | X | X | X
|
||||
# #others.uniq | X | X | X
|
||||
# #others.reset | X | X | X
|
||||
#
|
||||
@@ -652,7 +652,7 @@ module ActiveRecord
|
||||
# Returns the number of associated objects.
|
||||
# [collection.find(...)]
|
||||
# Finds an associated object according to the same rules as ActiveRecord::Base.find.
|
||||
# [collection.exist?(...)]
|
||||
# [collection.exists?(...)]
|
||||
# Checks whether an associated object with the given conditions exists.
|
||||
# Uses the same rules as ActiveRecord::Base.exists?.
|
||||
# [collection.build(attributes = {}, ...)]
|
||||
@@ -682,7 +682,7 @@ module ActiveRecord
|
||||
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
||||
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
||||
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
|
||||
# * <tt>Firm#clients.exist?(:name => 'ACME')</tt> (similar to <tt>Client.exist?(:name => 'ACME', :firm_id => firm.id)</tt>)
|
||||
# * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>)
|
||||
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
||||
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
@@ -1107,7 +1107,7 @@ module ActiveRecord
|
||||
# Finds an associated object responding to the +id+ and that
|
||||
# meets the condition that it has to be associated with this object.
|
||||
# Uses the same rules as ActiveRecord::Base.find.
|
||||
# [collection.exist?(...)]
|
||||
# [collection.exists?(...)]
|
||||
# Checks whether an associated object with the given conditions exists.
|
||||
# Uses the same rules as ActiveRecord::Base.exists?.
|
||||
# [collection.build(attributes = {})]
|
||||
@@ -1133,7 +1133,7 @@ module ActiveRecord
|
||||
# * <tt>Developer#projects.empty?</tt>
|
||||
# * <tt>Developer#projects.size</tt>
|
||||
# * <tt>Developer#projects.find(id)</tt>
|
||||
# * <tt>Developer#clients.exist?(...)</tt>
|
||||
# * <tt>Developer#clients.exists?(...)</tt>
|
||||
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
|
||||
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
|
||||
# The declaration may include an options hash to specialize the behavior of the association.
|
||||
@@ -1216,11 +1216,11 @@ module ActiveRecord
|
||||
# callbacks will be executed after the association is wiped out.
|
||||
old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
|
||||
class_eval <<-end_eval unless method_defined?(old_method)
|
||||
alias_method :#{old_method}, :destroy_without_callbacks
|
||||
def destroy_without_callbacks
|
||||
#{reflection.name}.clear
|
||||
#{old_method}
|
||||
end
|
||||
alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks
|
||||
def destroy_without_callbacks # def destroy_without_callbacks
|
||||
#{reflection.name}.clear # posts.clear
|
||||
#{old_method} # destroy_without_habtm_shim_for_posts
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
add_association_callbacks(reflection.name, options)
|
||||
@@ -1453,7 +1453,7 @@ module ActiveRecord
|
||||
dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions]
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
case reflection.options[:dependent]
|
||||
when :destroy
|
||||
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
||||
@@ -1463,22 +1463,22 @@ module ActiveRecord
|
||||
before_destroy method_name
|
||||
when :delete_all
|
||||
module_eval %Q{
|
||||
before_destroy do |record|
|
||||
delete_all_has_many_dependencies(record,
|
||||
"#{reflection.name}",
|
||||
#{reflection.class_name},
|
||||
"#{dependent_conditions}")
|
||||
end
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
when :nullify
|
||||
module_eval %Q{
|
||||
before_destroy do |record|
|
||||
nullify_has_many_dependencies(record,
|
||||
"#{reflection.name}",
|
||||
#{reflection.class_name},
|
||||
"#{reflection.primary_key_name}",
|
||||
"#{dependent_conditions}")
|
||||
end
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
"#{reflection.primary_key_name}", # "user_id",
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
else
|
||||
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
|
||||
@@ -1731,6 +1731,11 @@ module ActiveRecord
|
||||
return sanitize_sql(sql)
|
||||
end
|
||||
|
||||
def tables_in_string(string)
|
||||
return [] if string.blank?
|
||||
string.scan(/([\.a-zA-Z_]+).?\./).flatten
|
||||
end
|
||||
|
||||
def conditions_tables(options)
|
||||
# look in both sets of conditions
|
||||
conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
|
||||
@@ -1741,37 +1746,55 @@ module ActiveRecord
|
||||
else all << cond
|
||||
end
|
||||
end
|
||||
conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
|
||||
tables_in_string(conditions.join(' '))
|
||||
end
|
||||
|
||||
def order_tables(options)
|
||||
order = [options[:order], scope(:find, :order) ].join(", ")
|
||||
return [] unless order && order.is_a?(String)
|
||||
order.scan(/([\.a-zA-Z_]+).?\./).flatten
|
||||
tables_in_string(order)
|
||||
end
|
||||
|
||||
def selects_tables(options)
|
||||
select = options[:select]
|
||||
return [] unless select && select.is_a?(String)
|
||||
select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
|
||||
tables_in_string(select)
|
||||
end
|
||||
|
||||
def joined_tables(options)
|
||||
scope = scope(:find)
|
||||
joins = options[:joins]
|
||||
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
|
||||
[table_name] + case merged_joins
|
||||
when Symbol, Hash, Array
|
||||
if array_of_strings?(merged_joins)
|
||||
tables_in_string(merged_joins.join(' '))
|
||||
else
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil)
|
||||
join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact
|
||||
end
|
||||
else
|
||||
tables_in_string(merged_joins)
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if the conditions reference a table other than the current model table
|
||||
def include_eager_conditions?(options, tables = nil)
|
||||
((tables || conditions_tables(options)) - [table_name]).any?
|
||||
def include_eager_conditions?(options, tables = nil, joined_tables = nil)
|
||||
((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any?
|
||||
end
|
||||
|
||||
# Checks if the query order references a table other than the current model's table.
|
||||
def include_eager_order?(options, tables = nil)
|
||||
((tables || order_tables(options)) - [table_name]).any?
|
||||
def include_eager_order?(options, tables = nil, joined_tables = nil)
|
||||
((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any?
|
||||
end
|
||||
|
||||
def include_eager_select?(options)
|
||||
(selects_tables(options) - [table_name]).any?
|
||||
def include_eager_select?(options, joined_tables = nil)
|
||||
(selects_tables(options) - (joined_tables || joined_tables(options))).any?
|
||||
end
|
||||
|
||||
def references_eager_loaded_tables?(options)
|
||||
include_eager_order?(options) || include_eager_conditions?(options) || include_eager_select?(options)
|
||||
joined_tables = joined_tables(options)
|
||||
include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables)
|
||||
end
|
||||
|
||||
def using_limitable_reflections?(reflections)
|
||||
@@ -2148,7 +2171,7 @@ module ActiveRecord
|
||||
aliased_table_name,
|
||||
foreign_key,
|
||||
parent.aliased_table_name,
|
||||
parent.primary_key
|
||||
reflection.options[:primary_key] || parent.primary_key
|
||||
]
|
||||
end
|
||||
when :belongs_to
|
||||
@@ -2175,7 +2198,7 @@ module ActiveRecord
|
||||
protected
|
||||
|
||||
def aliased_table_name_for(name, suffix = nil)
|
||||
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{name.downcase}\son}
|
||||
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son}
|
||||
@join_dependency.table_aliases[name] += 1
|
||||
end
|
||||
|
||||
|
||||
@@ -83,7 +83,11 @@ module ActiveRecord
|
||||
|
||||
def to_ary
|
||||
load_target
|
||||
@target.to_ary
|
||||
if @target.is_a?(Array)
|
||||
@target.to_ary
|
||||
else
|
||||
Array(@target)
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
|
||||
@@ -180,7 +180,10 @@ module ActiveRecord
|
||||
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
|
||||
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
|
||||
else
|
||||
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
||||
unless @owner.new_record?
|
||||
primary_key = @reflection.options[:primary_key] || :id
|
||||
record[@reflection.primary_key_name] = @owner.send(primary_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -160,9 +160,9 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
"INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
|
||||
@reflection.through_reflection.table_name,
|
||||
@reflection.table_name, reflection_primary_key,
|
||||
@reflection.through_reflection.table_name, source_primary_key,
|
||||
@reflection.through_reflection.quoted_table_name,
|
||||
@reflection.quoted_table_name, reflection_primary_key,
|
||||
@reflection.through_reflection.quoted_table_name, source_primary_key,
|
||||
polymorphic_join
|
||||
]
|
||||
end
|
||||
|
||||
@@ -811,8 +811,7 @@ module ActiveRecord #:nodoc:
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +updates+ - A string of column and value pairs that will be set on any records that match conditions.
|
||||
# What goes into the SET clause.
|
||||
# * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
|
||||
# * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
|
||||
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
|
||||
#
|
||||
@@ -1417,8 +1416,8 @@ module ActiveRecord #:nodoc:
|
||||
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
|
||||
if logger && logger.level <= log_level
|
||||
result = nil
|
||||
seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
|
||||
logger.add(log_level, "#{title} (#{'%.1f' % (seconds * 1000)}ms)")
|
||||
ms = Benchmark.ms { result = use_silence ? silence { yield } : yield }
|
||||
logger.add(log_level, '%s (%.1fms)' % [title, ms])
|
||||
result
|
||||
else
|
||||
yield
|
||||
@@ -1457,7 +1456,10 @@ module ActiveRecord #:nodoc:
|
||||
def respond_to?(method_id, include_private = false)
|
||||
if match = DynamicFinderMatch.match(method_id)
|
||||
return true if all_attributes_exists?(match.attribute_names)
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
return true if all_attributes_exists?(match.attribute_names)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
@@ -1495,11 +1497,16 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
if scoped?(:find, :order)
|
||||
scoped_order = reverse_sql_order(scope(:find, :order))
|
||||
scoped_methods.select { |s| s[:find].update(:order => scoped_order) }
|
||||
scope = scope(:find)
|
||||
original_scoped_order = scope[:order]
|
||||
scope[:order] = reverse_sql_order(original_scoped_order)
|
||||
end
|
||||
|
||||
find_initial(options.merge({ :order => order }))
|
||||
begin
|
||||
find_initial(options.merge({ :order => order }))
|
||||
ensure
|
||||
scope[:order] = original_scoped_order if original_scoped_order
|
||||
end
|
||||
end
|
||||
|
||||
def reverse_sql_order(order_query)
|
||||
@@ -1805,7 +1812,11 @@ module ActiveRecord #:nodoc:
|
||||
# This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
|
||||
# or find_or_create_by_user_and_password(user, password).
|
||||
#
|
||||
# Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
|
||||
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
|
||||
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
|
||||
# respectively.
|
||||
#
|
||||
# Each dynamic finder, scope 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, &block)
|
||||
if match = DynamicFinderMatch.match(method_id)
|
||||
@@ -1814,10 +1825,31 @@ module ActiveRecord #:nodoc:
|
||||
if match.finder?
|
||||
finder = match.finder
|
||||
bang = match.bang?
|
||||
# def self.find_by_login_and_activated(*args)
|
||||
# options = args.extract_options!
|
||||
# attributes = construct_attributes_from_arguments(
|
||||
# [:login,:activated],
|
||||
# args
|
||||
# )
|
||||
# finder_options = { :conditions => attributes }
|
||||
# validate_find_options(options)
|
||||
# set_readonly_option!(options)
|
||||
#
|
||||
# if options[:conditions]
|
||||
# with_scope(:find => finder_options) do
|
||||
# find(:first, options)
|
||||
# end
|
||||
# else
|
||||
# find(:first, options.merge(finder_options))
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
def self.#{method_id}(*args)
|
||||
options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
|
||||
attributes = construct_attributes_from_arguments(
|
||||
[:#{attribute_names.join(',:')}],
|
||||
args
|
||||
)
|
||||
finder_options = { :conditions => attributes }
|
||||
validate_find_options(options)
|
||||
set_readonly_option!(options)
|
||||
@@ -1829,12 +1861,37 @@ module ActiveRecord #:nodoc:
|
||||
else
|
||||
find(:#{finder}, options.merge(finder_options))
|
||||
end
|
||||
#{'result || raise(RecordNotFound)' if bang}
|
||||
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
send(method_id, *arguments)
|
||||
elsif match.instantiator?
|
||||
instantiator = match.instantiator
|
||||
# def self.find_or_create_by_user_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(*[:user_id])
|
||||
# else
|
||||
# find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
|
||||
# end
|
||||
#
|
||||
# options = { :conditions => find_attributes }
|
||||
# set_readonly_option!(options)
|
||||
#
|
||||
# record = find(:first, options)
|
||||
#
|
||||
# if record.nil?
|
||||
# record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
||||
# yield(record) if block_given?
|
||||
# record.save
|
||||
# record
|
||||
# else
|
||||
# record
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
def self.#{method_id}(*args)
|
||||
guard_protected_attributes = false
|
||||
@@ -1864,6 +1921,22 @@ module ActiveRecord #:nodoc:
|
||||
}, __FILE__, __LINE__
|
||||
send(method_id, *arguments, &block)
|
||||
end
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
attribute_names = match.attribute_names
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
if match.scope?
|
||||
self.class_eval %{
|
||||
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
|
||||
[:#{attribute_names.join(',:')}], args # [:user_name, :password], args
|
||||
) # )
|
||||
#
|
||||
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
||||
end # end
|
||||
}, __FILE__, __LINE__
|
||||
send(method_id, *arguments)
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
@@ -2052,10 +2125,10 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
# Sets the default options for the model. The format of the
|
||||
# <tt>method_scoping</tt> argument is the same as in with_scope.
|
||||
# <tt>options</tt> argument is the same as in find.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# default_scope :find => { :order => 'last_name, first_name' }
|
||||
# default_scope :order => 'last_name, first_name'
|
||||
# end
|
||||
def default_scope(options = {})
|
||||
self.default_scoping << { :find => options, :create => (options.is_a?(Hash) && options.has_key?(:conditions)) ? options[:conditions] : {} }
|
||||
@@ -2402,9 +2475,9 @@ module ActiveRecord #:nodoc:
|
||||
write_attribute(self.class.primary_key, value)
|
||||
end
|
||||
|
||||
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
|
||||
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false.
|
||||
def new_record?
|
||||
defined?(@new_record) && @new_record
|
||||
@new_record || false
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
@@ -3011,7 +3084,7 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
Base.class_eval do
|
||||
extend QueryCache
|
||||
extend QueryCache::ClassMethods
|
||||
include Validations
|
||||
include Locking::Optimistic, Locking::Pessimistic
|
||||
include AttributeMethods
|
||||
|
||||
@@ -123,6 +123,7 @@ module ActiveRecord
|
||||
connection_handler.retrieve_connection(self)
|
||||
end
|
||||
|
||||
# Returns true if +ActiveRecord+ is connected.
|
||||
def connected?
|
||||
connection_handler.connected?(self)
|
||||
end
|
||||
|
||||
@@ -14,12 +14,12 @@ module ActiveRecord
|
||||
def dirties_query_cache(base, *method_names)
|
||||
method_names.each do |method_name|
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__
|
||||
def #{method_name}_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args)
|
||||
end
|
||||
|
||||
alias_method_chain :#{method_name}, :query_dirty
|
||||
def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
|
||||
end # end
|
||||
#
|
||||
alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty
|
||||
end_code
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,10 +32,12 @@ module ActiveRecord
|
||||
@primary = nil
|
||||
end
|
||||
|
||||
# Returns +true+ if the column is either of type string or text.
|
||||
def text?
|
||||
type == :string || type == :text
|
||||
end
|
||||
|
||||
# Returns +true+ if the column is either of type integer, float or decimal.
|
||||
def number?
|
||||
type == :integer || type == :float || type == :decimal
|
||||
end
|
||||
@@ -295,7 +297,7 @@ module ActiveRecord
|
||||
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# def self.down
|
||||
# ...
|
||||
# end
|
||||
@@ -474,12 +476,12 @@ module ActiveRecord
|
||||
|
||||
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
||||
class_eval <<-EOV
|
||||
def #{column_type}(*args)
|
||||
options = args.extract_options!
|
||||
column_names = args
|
||||
|
||||
column_names.each { |name| column(name, '#{column_type}', options) }
|
||||
end
|
||||
def #{column_type}(*args) # def string(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
column_names = args # column_names = args
|
||||
#
|
||||
column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
|
||||
end # end
|
||||
EOV
|
||||
end
|
||||
|
||||
@@ -674,24 +676,24 @@ module ActiveRecord
|
||||
# t.string(:goat, :sheep)
|
||||
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
||||
class_eval <<-EOV
|
||||
def #{column_type}(*args)
|
||||
options = args.extract_options!
|
||||
column_names = args
|
||||
|
||||
column_names.each do |name|
|
||||
column = ColumnDefinition.new(@base, name, '#{column_type}')
|
||||
if options[:limit]
|
||||
column.limit = options[:limit]
|
||||
elsif native['#{column_type}'.to_sym].is_a?(Hash)
|
||||
column.limit = native['#{column_type}'.to_sym][:limit]
|
||||
end
|
||||
column.precision = options[:precision]
|
||||
column.scale = options[:scale]
|
||||
column.default = options[:default]
|
||||
column.null = options[:null]
|
||||
@base.add_column(@table_name, name, column.sql_type, options)
|
||||
end
|
||||
end
|
||||
def #{column_type}(*args) # def string(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
column_names = args # column_names = args
|
||||
#
|
||||
column_names.each do |name| # column_names.each do |name|
|
||||
column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
|
||||
if options[:limit] # if options[:limit]
|
||||
column.limit = options[:limit] # column.limit = options[:limit]
|
||||
elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
|
||||
column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
|
||||
end # end
|
||||
column.precision = options[:precision] # column.precision = options[:precision]
|
||||
column.scale = options[:scale] # column.scale = options[:scale]
|
||||
column.default = options[:default] # column.default = options[:default]
|
||||
column.null = options[:null] # column.null = options[:null]
|
||||
@base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
|
||||
end # end
|
||||
end # end
|
||||
EOV
|
||||
end
|
||||
|
||||
|
||||
@@ -184,9 +184,9 @@ module ActiveRecord
|
||||
# for more information about the effect of this option.
|
||||
attr_accessor :transactional_fixtures
|
||||
|
||||
def log_info(sql, name, seconds)
|
||||
def log_info(sql, name, ms)
|
||||
if @logger && @logger.debug?
|
||||
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%.1f", seconds * 1000)}ms)"
|
||||
name = '%s (%.1fms)' % [name || 'SQL', ms]
|
||||
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
|
||||
end
|
||||
end
|
||||
@@ -195,9 +195,9 @@ module ActiveRecord
|
||||
def log(sql, name)
|
||||
if block_given?
|
||||
result = nil
|
||||
seconds = Benchmark.realtime { result = yield }
|
||||
@runtime += seconds
|
||||
log_info(sql, name, seconds)
|
||||
ms = Benchmark.ms { result = yield }
|
||||
@runtime += ms
|
||||
log_info(sql, name, ms)
|
||||
result
|
||||
else
|
||||
log_info(sql, name, 0)
|
||||
|
||||
@@ -13,23 +13,25 @@ module MysqlCompat #:nodoc:
|
||||
# C driver >= 2.7 returns null values in each_hash
|
||||
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
|
||||
target.class_eval <<-'end_eval'
|
||||
def all_hashes
|
||||
rows = []
|
||||
each_hash { |row| rows << row }
|
||||
rows
|
||||
end
|
||||
def all_hashes # def all_hashes
|
||||
rows = [] # rows = []
|
||||
each_hash { |row| rows << row } # each_hash { |row| rows << row }
|
||||
rows # rows
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
# adapters before 2.7 don't have a version constant
|
||||
# and don't return null values in each_hash
|
||||
else
|
||||
target.class_eval <<-'end_eval'
|
||||
def all_hashes
|
||||
rows = []
|
||||
all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
|
||||
each_hash { |row| rows << all_fields.dup.update(row) }
|
||||
rows
|
||||
end
|
||||
def all_hashes # def all_hashes
|
||||
rows = [] # rows = []
|
||||
all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
|
||||
fields[f.name] = nil; fields # fields[f.name] = nil; fields
|
||||
} # }
|
||||
each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
|
||||
rows # rows
|
||||
end # end
|
||||
end_eval
|
||||
end
|
||||
|
||||
@@ -312,6 +314,7 @@ module ActiveRecord
|
||||
rows
|
||||
end
|
||||
|
||||
# Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it.
|
||||
def execute(sql, name = nil) #:nodoc:
|
||||
log(sql, name) { @connection.query(sql) }
|
||||
rescue ActiveRecord::StatementInvalid => exception
|
||||
@@ -429,7 +432,9 @@ module ActiveRecord
|
||||
|
||||
def tables(name = nil) #:nodoc:
|
||||
tables = []
|
||||
execute("SHOW TABLES", name).each { |field| tables << field[0] }
|
||||
result = execute("SHOW TABLES", name)
|
||||
result.each { |field| tables << field[0] }
|
||||
result.free
|
||||
tables
|
||||
end
|
||||
|
||||
@@ -440,7 +445,8 @@ module ActiveRecord
|
||||
def indexes(table_name, name = nil)#:nodoc:
|
||||
indexes = []
|
||||
current_index = nil
|
||||
execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name).each do |row|
|
||||
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
|
||||
result.each do |row|
|
||||
if current_index != row[2]
|
||||
next if row[2] == "PRIMARY" # skip the primary key
|
||||
current_index = row[2]
|
||||
@@ -449,13 +455,16 @@ module ActiveRecord
|
||||
|
||||
indexes.last.columns << row[4]
|
||||
end
|
||||
result.free
|
||||
indexes
|
||||
end
|
||||
|
||||
def columns(table_name, name = nil)#:nodoc:
|
||||
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
|
||||
columns = []
|
||||
execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
|
||||
result = execute(sql, name)
|
||||
result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
|
||||
result.free
|
||||
columns
|
||||
end
|
||||
|
||||
@@ -536,9 +545,11 @@ module ActiveRecord
|
||||
# Returns a table's primary key and belonging sequence.
|
||||
def pk_and_sequence_for(table) #:nodoc:
|
||||
keys = []
|
||||
execute("describe #{quote_table_name(table)}").each_hash do |h|
|
||||
result = execute("describe #{quote_table_name(table)}")
|
||||
result.each_hash do |h|
|
||||
keys << h["Field"]if h["Key"] == "PRI"
|
||||
end
|
||||
result.free
|
||||
keys.length == 1 ? [keys.first, nil] : nil
|
||||
end
|
||||
|
||||
|
||||
@@ -937,13 +937,13 @@ module ActiveRecord
|
||||
# should know about this but can't detect it there, so deal with it here.
|
||||
money_precision = (postgresql_version >= 80300) ? 19 : 10
|
||||
PostgreSQLColumn.module_eval(<<-end_eval)
|
||||
def extract_precision(sql_type)
|
||||
if sql_type =~ /^money$/
|
||||
#{money_precision}
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
def extract_precision(sql_type) # def extract_precision(sql_type)
|
||||
if sql_type =~ /^money$/ # if sql_type =~ /^money$/
|
||||
#{money_precision} # 19
|
||||
else # else
|
||||
super # super
|
||||
end # end
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
configure_connection
|
||||
|
||||
@@ -402,6 +402,10 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def add_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
|
||||
raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
|
||||
end
|
||||
|
||||
alter_table(table_name) do |definition|
|
||||
definition.column(column_name, type, options)
|
||||
end
|
||||
|
||||
@@ -174,7 +174,7 @@ module ActiveRecord
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_SUFFIXES.each do |suffix|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||
STR
|
||||
end
|
||||
end
|
||||
|
||||
25
activerecord/lib/active_record/dynamic_scope_match.rb
Normal file
25
activerecord/lib/active_record/dynamic_scope_match.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module ActiveRecord
|
||||
class DynamicScopeMatch
|
||||
def self.match(method)
|
||||
ds_match = self.new(method)
|
||||
ds_match.scope ? ds_match : nil
|
||||
end
|
||||
|
||||
def initialize(method)
|
||||
@scope = true
|
||||
case method.to_s
|
||||
when /^scoped_by_([_a-zA-Z]\w*)$/
|
||||
names = $1
|
||||
else
|
||||
@scope = nil
|
||||
end
|
||||
@attribute_names = names && names.split('_and_')
|
||||
end
|
||||
|
||||
attr_reader :scope, :attribute_names
|
||||
|
||||
def scope?
|
||||
!@scope.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +1,32 @@
|
||||
module ActiveRecord
|
||||
module QueryCache
|
||||
# Enable the query cache within the block if Active Record is configured.
|
||||
def cache(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.cache(&block)
|
||||
class QueryCache
|
||||
module ClassMethods
|
||||
# Enable the query cache within the block if Active Record is configured.
|
||||
def cache(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.cache(&block)
|
||||
end
|
||||
end
|
||||
|
||||
# Disable the query cache within the block if Active Record is configured.
|
||||
def uncached(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.uncached(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Disable the query cache within the block if Active Record is configured.
|
||||
def uncached(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.uncached(&block)
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
ActiveRecord::Base.cache do
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,11 +23,12 @@ module ActiveRecord #:nodoc:
|
||||
# </topic>
|
||||
#
|
||||
# This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
|
||||
# <tt>:skip_instruct</tt>, <tt>:skip_types</tt> and <tt>:dasherize</tt>.
|
||||
# <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
|
||||
# The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
|
||||
# +attributes+ method. The default is to dasherize all column names, but you
|
||||
# can disable this setting <tt>:dasherize</tt> to +false+. To not have the
|
||||
# column type included in the XML output set <tt>:skip_types</tt> to +true+.
|
||||
# can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
|
||||
# to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
|
||||
# To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
|
||||
#
|
||||
# For instance:
|
||||
#
|
||||
@@ -178,13 +179,22 @@ module ActiveRecord #:nodoc:
|
||||
|
||||
def root
|
||||
root = (options[:root] || @record.class.to_s.underscore).to_s
|
||||
dasherize? ? root.dasherize : root
|
||||
reformat_name(root)
|
||||
end
|
||||
|
||||
def dasherize?
|
||||
!options.has_key?(:dasherize) || options[:dasherize]
|
||||
end
|
||||
|
||||
def camelize?
|
||||
options.has_key?(:camelize) && options[:camelize]
|
||||
end
|
||||
|
||||
def reformat_name(name)
|
||||
name = name.camelize if camelize?
|
||||
dasherize? ? name.dasherize : name
|
||||
end
|
||||
|
||||
def serializable_attributes
|
||||
serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
|
||||
end
|
||||
@@ -212,7 +222,7 @@ module ActiveRecord #:nodoc:
|
||||
|
||||
def add_tag(attribute)
|
||||
builder.tag!(
|
||||
dasherize? ? attribute.name.dasherize : attribute.name,
|
||||
reformat_name(attribute.name),
|
||||
attribute.value.to_s,
|
||||
attribute.decorations(!options[:skip_types])
|
||||
)
|
||||
@@ -220,8 +230,7 @@ module ActiveRecord #:nodoc:
|
||||
|
||||
def add_associations(association, records, opts)
|
||||
if records.is_a?(Enumerable)
|
||||
tag = association.to_s
|
||||
tag = tag.dasherize if dasherize?
|
||||
tag = reformat_name(association.to_s)
|
||||
if records.empty?
|
||||
builder.tag!(tag, :type => :array)
|
||||
else
|
||||
|
||||
319
activerecord/lib/active_record/session_store.rb
Normal file
319
activerecord/lib/active_record/session_store.rb
Normal file
@@ -0,0 +1,319 @@
|
||||
module ActiveRecord
|
||||
# A session store backed by an Active Record class. A default class is
|
||||
# provided, but any object duck-typing to an Active Record Session class
|
||||
# with text +session_id+ and +data+ attributes is sufficient.
|
||||
#
|
||||
# The default assumes a +sessions+ tables with columns:
|
||||
# +id+ (numeric primary key),
|
||||
# +session_id+ (text, or longtext if your session data exceeds 65K), and
|
||||
# +data+ (text or longtext; careful if your session data exceeds 65KB).
|
||||
# The +session_id+ column should always be indexed for speedy lookups.
|
||||
# Session data is marshaled to the +data+ column in Base64 format.
|
||||
# If the data you write is larger than the column's size limit,
|
||||
# ActionController::SessionOverflowError will be raised.
|
||||
#
|
||||
# You may configure the table name, primary key, and data column.
|
||||
# For example, at the end of <tt>config/environment.rb</tt>:
|
||||
# ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
|
||||
# ActiveRecord::SessionStore::Session.primary_key = 'session_id'
|
||||
# ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
|
||||
# Note that setting the primary key to the +session_id+ frees you from
|
||||
# having a separate +id+ column if you don't want it. However, you must
|
||||
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
|
||||
# on ApplicationController is a good place.
|
||||
#
|
||||
# Since the default class is a simple Active Record, you get timestamps
|
||||
# for free if you add +created_at+ and +updated_at+ datetime columns to
|
||||
# the +sessions+ table, making periodic session expiration a snap.
|
||||
#
|
||||
# You may provide your own session class implementation, whether a
|
||||
# feature-packed Active Record or a bare-metal high-performance SQL
|
||||
# store, by setting
|
||||
# ActiveRecord::SessionStore.session_class = MySessionClass
|
||||
# You must implement these methods:
|
||||
# self.find_by_session_id(session_id)
|
||||
# initialize(hash_of_session_id_and_data)
|
||||
# attr_reader :session_id
|
||||
# attr_accessor :data
|
||||
# save
|
||||
# destroy
|
||||
#
|
||||
# The example SqlBypass class is a generic SQL session store. You may
|
||||
# use it as a basis for high-performance database-specific stores.
|
||||
class SessionStore < ActionController::Session::AbstractStore
|
||||
# The default Active Record class.
|
||||
class Session < ActiveRecord::Base
|
||||
##
|
||||
# :singleton-method:
|
||||
# Customizable data column name. Defaults to 'data'.
|
||||
cattr_accessor :data_column_name
|
||||
self.data_column_name = 'data'
|
||||
|
||||
before_save :marshal_data!
|
||||
before_save :raise_on_session_data_overflow!
|
||||
|
||||
class << self
|
||||
# Don't try to reload ARStore::Session in dev mode.
|
||||
def reloadable? #:nodoc:
|
||||
false
|
||||
end
|
||||
|
||||
def data_column_size_limit
|
||||
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
|
||||
end
|
||||
|
||||
# Hook to set up sessid compatibility.
|
||||
def find_by_session_id(session_id)
|
||||
setup_sessid_compatibility!
|
||||
find_by_session_id(session_id)
|
||||
end
|
||||
|
||||
def marshal(data)
|
||||
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
|
||||
end
|
||||
|
||||
def unmarshal(data)
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
|
||||
end
|
||||
|
||||
def create_table!
|
||||
connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
|
||||
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
|
||||
private
|
||||
# Compatibility with tables using sessid instead of session_id.
|
||||
def setup_sessid_compatibility!
|
||||
# Reset column info since it may be stale.
|
||||
reset_column_information
|
||||
if columns_hash['sessid']
|
||||
def self.find_by_session_id(*args)
|
||||
find_by_sessid(*args)
|
||||
end
|
||||
|
||||
define_method(:session_id) { sessid }
|
||||
define_method(:session_id=) { |session_id| self.sessid = session_id }
|
||||
else
|
||||
def self.find_by_session_id(session_id)
|
||||
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
|
||||
end
|
||||
|
||||
attr_writer :data
|
||||
|
||||
# Has the session been loaded yet?
|
||||
def loaded?
|
||||
!!@data
|
||||
end
|
||||
|
||||
private
|
||||
def marshal_data!
|
||||
return false if !loaded?
|
||||
write_attribute(@@data_column_name, self.class.marshal(self.data))
|
||||
end
|
||||
|
||||
# Ensures that the data about to be stored in the database is not
|
||||
# larger than the data storage column. Raises
|
||||
# ActionController::SessionOverflowError.
|
||||
def raise_on_session_data_overflow!
|
||||
return false if !loaded?
|
||||
limit = self.class.data_column_size_limit
|
||||
if loaded? and limit and read_attribute(@@data_column_name).size > limit
|
||||
raise ActionController::SessionOverflowError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A barebones session store which duck-types with the default session
|
||||
# store but bypasses Active Record and issues SQL directly. This is
|
||||
# an example session model class meant as a basis for your own classes.
|
||||
#
|
||||
# The database connection, table name, and session id and data columns
|
||||
# are configurable class attributes. Marshaling and unmarshaling
|
||||
# are implemented as class methods that you may override. By default,
|
||||
# marshaling data is
|
||||
#
|
||||
# ActiveSupport::Base64.encode64(Marshal.dump(data))
|
||||
#
|
||||
# and unmarshaling data is
|
||||
#
|
||||
# Marshal.load(ActiveSupport::Base64.decode64(data))
|
||||
#
|
||||
# This marshaling behavior is intended to store the widest range of
|
||||
# binary session data in a +text+ column. For higher performance,
|
||||
# store in a +blob+ column instead and forgo the Base64 encoding.
|
||||
class SqlBypass
|
||||
##
|
||||
# :singleton-method:
|
||||
# Use the ActiveRecord::Base.connection by default.
|
||||
cattr_accessor :connection
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The table name defaults to 'sessions'.
|
||||
cattr_accessor :table_name
|
||||
@@table_name = 'sessions'
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The session id field defaults to 'session_id'.
|
||||
cattr_accessor :session_id_column
|
||||
@@session_id_column = 'session_id'
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The data field defaults to 'data'.
|
||||
cattr_accessor :data_column
|
||||
@@data_column = 'data'
|
||||
|
||||
class << self
|
||||
def connection
|
||||
@@connection ||= ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
# Look up a session by id and unmarshal its data if found.
|
||||
def find_by_session_id(session_id)
|
||||
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
|
||||
new(:session_id => session_id, :marshaled_data => record['data'])
|
||||
end
|
||||
end
|
||||
|
||||
def marshal(data)
|
||||
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
|
||||
end
|
||||
|
||||
def unmarshal(data)
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
|
||||
end
|
||||
|
||||
def create_table!
|
||||
@@connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
|
||||
#{@@connection.quote_column_name(data_column)} TEXT
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
@@connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :session_id
|
||||
attr_writer :data
|
||||
|
||||
# Look for normal and marshaled data, self.find_by_session_id's way of
|
||||
# telling us to postpone unmarshaling until the data is requested.
|
||||
# We need to handle a normal data attribute in case of a new record.
|
||||
def initialize(attributes)
|
||||
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
|
||||
@new_record = @marshaled_data.nil?
|
||||
end
|
||||
|
||||
def new_record?
|
||||
@new_record
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
unless @data
|
||||
if @marshaled_data
|
||||
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
|
||||
else
|
||||
@data = {}
|
||||
end
|
||||
end
|
||||
@data
|
||||
end
|
||||
|
||||
def loaded?
|
||||
!!@data
|
||||
end
|
||||
|
||||
def save
|
||||
return false if !loaded?
|
||||
marshaled_data = self.class.marshal(data)
|
||||
|
||||
if @new_record
|
||||
@new_record = false
|
||||
@@connection.update <<-end_sql, 'Create session'
|
||||
INSERT INTO #{@@table_name} (
|
||||
#{@@connection.quote_column_name(@@session_id_column)},
|
||||
#{@@connection.quote_column_name(@@data_column)} )
|
||||
VALUES (
|
||||
#{@@connection.quote(session_id)},
|
||||
#{@@connection.quote(marshaled_data)} )
|
||||
end_sql
|
||||
else
|
||||
@@connection.update <<-end_sql, 'Update session'
|
||||
UPDATE #{@@table_name}
|
||||
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @new_record
|
||||
@@connection.delete <<-end_sql, 'Destroy session'
|
||||
DELETE FROM #{@@table_name}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The class used for session storage. Defaults to
|
||||
# ActiveRecord::SessionStore::Session
|
||||
cattr_accessor :session_class
|
||||
self.session_class = Session
|
||||
|
||||
SESSION_RECORD_KEY = 'rack.session.record'.freeze
|
||||
|
||||
private
|
||||
def get_session(env, sid)
|
||||
Base.silence do
|
||||
sid ||= generate_sid
|
||||
session = @@session_class.find_by_session_id(sid)
|
||||
session ||= @@session_class.new(:session_id => sid, :data => {})
|
||||
env[SESSION_RECORD_KEY] = session
|
||||
[sid, session.data]
|
||||
end
|
||||
end
|
||||
|
||||
def set_session(env, sid, session_data)
|
||||
Base.silence do
|
||||
record = env[SESSION_RECORD_KEY]
|
||||
record.data = session_data
|
||||
return false unless record.save
|
||||
|
||||
session_data = record.data
|
||||
if session_data && session_data.respond_to?(:each_value)
|
||||
session_data.each_value do |obj|
|
||||
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -23,8 +23,8 @@ module ActiveRecord
|
||||
write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
|
||||
write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
|
||||
|
||||
write_attribute('updated_at', t) if respond_to?(:updated_at)
|
||||
write_attribute('updated_on', t) if respond_to?(:updated_on)
|
||||
write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil?
|
||||
write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil?
|
||||
end
|
||||
create_without_timestamps
|
||||
end
|
||||
|
||||
@@ -200,7 +200,7 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def save_with_transactions! #:nodoc:
|
||||
rollback_active_record_state! { transaction { save_without_transactions! } }
|
||||
rollback_active_record_state! { self.class.transaction { save_without_transactions! } }
|
||||
end
|
||||
|
||||
# Reset id and @new_record if the transaction rolls back.
|
||||
@@ -228,7 +228,7 @@ module ActiveRecord
|
||||
# instance.
|
||||
def with_transaction_returning_status(method, *args)
|
||||
status = nil
|
||||
transaction do
|
||||
self.class.transaction do
|
||||
status = send(method, *args)
|
||||
raise ActiveRecord::Rollback unless status
|
||||
end
|
||||
|
||||
@@ -203,7 +203,6 @@ module ActiveRecord
|
||||
if attr == "base"
|
||||
full_messages << message
|
||||
else
|
||||
#key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
|
||||
attr_name = @base.class.human_attribute_name(attr)
|
||||
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
|
||||
end
|
||||
@@ -1049,15 +1048,15 @@ module ActiveRecord
|
||||
|
||||
protected
|
||||
# Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes.
|
||||
def validate #:doc:
|
||||
def validate
|
||||
end
|
||||
|
||||
# Overwrite this method for validation checks used only on creation.
|
||||
def validate_on_create #:doc:
|
||||
def validate_on_create
|
||||
end
|
||||
|
||||
# Overwrite this method for validation checks used only on updates.
|
||||
def validate_on_update # :doc:
|
||||
def validate_on_update
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -104,6 +104,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
|
||||
authors.first.posts.first.special_comments.first.post.very_special_comment
|
||||
end
|
||||
end
|
||||
|
||||
def test_eager_association_loading_where_first_level_returns_nil
|
||||
authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
|
||||
assert_equal [authors(:mary), authors(:david)], authors
|
||||
assert_no_queries do
|
||||
authors[1].post_about_thinking.comments.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'models/vertex'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require "cases/helper"
|
||||
require 'models/post'
|
||||
require 'models/tagging'
|
||||
require 'models/tag'
|
||||
require 'models/comment'
|
||||
require 'models/author'
|
||||
require 'models/category'
|
||||
@@ -145,7 +146,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
||||
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
|
||||
post = posts(:welcome)
|
||||
post.update_attributes!(:author => nil)
|
||||
post = assert_queries(2) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the address
|
||||
post = assert_queries(1) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the author or address
|
||||
assert_no_queries do
|
||||
assert_equal nil, post.author_with_address
|
||||
end
|
||||
@@ -705,4 +706,117 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
||||
def test_order_on_join_table_with_include_and_limit
|
||||
assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size
|
||||
end
|
||||
|
||||
def test_eager_loading_with_order_on_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC')
|
||||
end
|
||||
assert_equal posts(:eager_other), posts[0]
|
||||
assert_equal authors(:mary), assert_no_queries { posts[0].author}
|
||||
end
|
||||
|
||||
def test_eager_loading_with_conditions_on_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal posts(:welcome, :thinking), posts
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id')
|
||||
end
|
||||
assert_equal posts(:welcome, :thinking), posts
|
||||
|
||||
end
|
||||
|
||||
def test_eager_loading_with_conditions_on_string_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
end
|
||||
|
||||
def test_eager_loading_with_select_on_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'posts.*, authors.name as author_name', :include => :comments, :joins => :author, :order => 'posts.id')
|
||||
end
|
||||
assert_equal 'David', posts[0].author_name
|
||||
assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments}
|
||||
end
|
||||
|
||||
def test_eager_loading_with_conditions_on_join_model_preloads
|
||||
authors = assert_queries(2) do
|
||||
Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'")
|
||||
end
|
||||
assert_equal authors(:david), authors[0]
|
||||
assert_equal author_addresses(:david_address), authors[0].author_address
|
||||
end
|
||||
|
||||
def test_preload_belongs_to_uses_exclusive_scope
|
||||
people = Person.males.find(:all, :include => :primary_contact)
|
||||
assert_not_equal people.length, 0
|
||||
people.each do |person|
|
||||
assert_no_queries {assert_not_nil person.primary_contact}
|
||||
assert_equal Person.find(person.id).primary_contact, person.primary_contact
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_has_many_uses_exclusive_scope
|
||||
people = Person.males.find :all, :include => :agents
|
||||
people.each do |person|
|
||||
assert_equal Person.find(person.id).agents, person.agents
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_has_many_using_primary_key
|
||||
expected = Firm.find(:first).clients_using_primary_key.to_a
|
||||
firm = Firm.find :first, :include => :clients_using_primary_key
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.clients_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_has_many_using_primary_key
|
||||
expected = Firm.find(1).clients_using_primary_key.sort_by &:name
|
||||
firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name'
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.clients_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_has_one_using_primary_key
|
||||
expected = Firm.find(:first).account_using_primary_key
|
||||
firm = Firm.find :first, :include => :account_using_primary_key
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.account_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_has_one_using_primary_key
|
||||
expected = Firm.find(1).account_using_primary_key
|
||||
firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').detect {|f| f.id == 1}
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.account_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -665,6 +665,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
||||
assert_equal 1, Client.find_all_by_client_of(firm.id).size
|
||||
end
|
||||
|
||||
def test_dependent_association_respects_optional_hash_conditions_on_delete
|
||||
firm = companies(:odegy)
|
||||
Client.create(:client_of => firm.id, :name => "BigShot Inc.")
|
||||
Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
|
||||
# only one of two clients is included in the association due to the :conditions key
|
||||
assert_equal 2, Client.find_all_by_client_of(firm.id).size
|
||||
assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
|
||||
firm.destroy
|
||||
# only the correctly associated client should have been deleted
|
||||
assert_equal 1, Client.find_all_by_client_of(firm.id).size
|
||||
end
|
||||
|
||||
|
||||
def test_creation_respects_hash_condition
|
||||
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
|
||||
|
||||
@@ -1102,5 +1115,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
||||
assert !client_association.respond_to?(:private_method)
|
||||
assert client_association.respond_to?(:private_method, true)
|
||||
end
|
||||
|
||||
def test_creating_using_primary_key
|
||||
firm = Firm.find(:first)
|
||||
client = firm.clients_using_primary_key.create!(:name => 'test')
|
||||
assert_equal firm.name, client.firm_name
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ require 'models/post'
|
||||
require 'models/person'
|
||||
require 'models/reader'
|
||||
require 'models/comment'
|
||||
require 'models/tag'
|
||||
require 'models/tagging'
|
||||
require 'models/author'
|
||||
|
||||
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :posts, :readers, :people, :comments, :authors
|
||||
@@ -201,6 +204,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
||||
assert_equal 2, people(:michael).posts.count(:include => :readers)
|
||||
end
|
||||
|
||||
def test_inner_join_with_quoted_table_name
|
||||
assert_equal 2, people(:michael).jobs.size
|
||||
end
|
||||
|
||||
def test_get_ids
|
||||
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require "cases/helper"
|
||||
require 'models/club'
|
||||
require 'models/member_type'
|
||||
require 'models/member'
|
||||
require 'models/membership'
|
||||
require 'models/sponsor'
|
||||
@@ -7,7 +8,7 @@ require 'models/organization'
|
||||
require 'models/member_detail'
|
||||
|
||||
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :members, :clubs, :memberships, :sponsors, :organizations
|
||||
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations
|
||||
|
||||
def setup
|
||||
@member = members(:groucho)
|
||||
@@ -158,4 +159,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
||||
assert @new_organization.members.include?(@member)
|
||||
end
|
||||
|
||||
def test_preloading_has_one_through_on_belongs_to
|
||||
assert_not_nil @member.member_type
|
||||
@organization = organizations(:nsa)
|
||||
@member_detail = MemberDetail.new
|
||||
@member.member_detail = @member_detail
|
||||
@member.organization = @organization
|
||||
@member_details = assert_queries(3) do
|
||||
MemberDetail.find(:all, :include => :member_type)
|
||||
end
|
||||
@new_detail = @member_details[0]
|
||||
assert @new_detail.loaded_member_type?
|
||||
assert_not_nil assert_no_queries { @new_detail.member_type }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -16,6 +16,7 @@ require 'models/post'
|
||||
require 'models/comment'
|
||||
require 'models/minimalistic'
|
||||
require 'models/warehouse_thing'
|
||||
require 'models/parrot'
|
||||
require 'rexml/document'
|
||||
|
||||
class Category < ActiveRecord::Base; end
|
||||
@@ -1197,6 +1198,11 @@ class BasicsTest < ActiveRecord::TestCase
|
||||
assert b_true.value?
|
||||
end
|
||||
|
||||
def test_new_record_returns_boolean
|
||||
assert_equal Topic.new.new_record?, true
|
||||
assert_equal Topic.find(1).new_record?, false
|
||||
end
|
||||
|
||||
def test_clone
|
||||
topic = Topic.find(1)
|
||||
cloned_topic = nil
|
||||
@@ -2071,6 +2077,15 @@ class BasicsTest < ActiveRecord::TestCase
|
||||
ActiveRecord::Base.logger = original_logger
|
||||
end
|
||||
|
||||
def test_create_with_custom_timestamps
|
||||
custom_datetime = 1.hour.ago.beginning_of_day
|
||||
|
||||
%w(created_at created_on updated_at updated_on).each do |attribute|
|
||||
parrot = LiveParrot.create(:name => "colombian", attribute => custom_datetime)
|
||||
assert_equal custom_datetime, parrot[attribute]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def with_kcode(kcode)
|
||||
if RUBY_VERSION < '1.9'
|
||||
|
||||
@@ -171,8 +171,9 @@ class CalculationsTest < ActiveRecord::TestCase
|
||||
Account.expects(:columns).at_least_once.returns([column])
|
||||
|
||||
c = Account.count(:all, :group => :firm)
|
||||
assert_equal Firm, c.first.first.class
|
||||
assert_equal 1, c.first.last
|
||||
first_key = c.keys.first
|
||||
assert_equal Firm, first_key.class
|
||||
assert_equal 1, c[first_key]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ require 'active_record/test_case'
|
||||
require 'active_record/fixtures'
|
||||
require 'connection'
|
||||
|
||||
require 'cases/repair_helper'
|
||||
|
||||
# Show backtraces for deprecated behavior for quicker cleanup.
|
||||
ActiveSupport::Deprecation.debug = true
|
||||
|
||||
@@ -24,6 +26,7 @@ end
|
||||
|
||||
def uses_mocha(description)
|
||||
require 'rubygems'
|
||||
gem 'mocha', '>= 0.9.3'
|
||||
require 'mocha'
|
||||
yield
|
||||
rescue LoadError
|
||||
@@ -59,6 +62,8 @@ end
|
||||
|
||||
class ActiveSupport::TestCase
|
||||
include ActiveRecord::TestFixtures
|
||||
include ActiveRecord::Testing::RepairHelper
|
||||
|
||||
self.fixture_path = FIXTURES_ROOT
|
||||
self.use_instantiated_fixtures = false
|
||||
self.use_transactional_fixtures = true
|
||||
|
||||
@@ -27,6 +27,24 @@ class MethodScopingTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_last
|
||||
highest_salary = Developer.find(:first, :order => "salary DESC")
|
||||
|
||||
Developer.with_scope(:find => { :order => "salary" }) do
|
||||
assert_equal highest_salary, Developer.last
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_last_preserves_scope
|
||||
lowest_salary = Developer.find(:first, :order => "salary ASC")
|
||||
highest_salary = Developer.find(:first, :order => "salary DESC")
|
||||
|
||||
Developer.with_scope(:find => { :order => "salary" }) do
|
||||
assert_equal highest_salary, Developer.last
|
||||
assert_equal lowest_salary, Developer.first
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_combines_conditions
|
||||
Developer.with_scope(:find => { :conditions => "salary = 9000" }) do
|
||||
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'")
|
||||
|
||||
@@ -254,7 +254,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
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)
|
||||
assert_equal Developer.find_all_by_name('Jamis').to_set, Developer.find_all_by_id(Developer.jamises).to_set
|
||||
end
|
||||
|
||||
def test_size_should_use_count_when_results_are_not_loaded
|
||||
@@ -278,3 +278,23 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
|
||||
end
|
||||
end
|
||||
|
||||
class DynamicScopeMatchTest < ActiveRecord::TestCase
|
||||
def test_scoped_by_no_match
|
||||
assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
|
||||
end
|
||||
|
||||
def test_scoped_by
|
||||
match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location")
|
||||
assert_not_nil match
|
||||
assert match.scope?
|
||||
assert_equal %w(age sex location), match.attribute_names
|
||||
end
|
||||
end
|
||||
|
||||
class DynamicScopeTest < ActiveRecord::TestCase
|
||||
def test_dynamic_scope
|
||||
assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)
|
||||
assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,8 @@ require 'models/owner'
|
||||
require 'models/pet'
|
||||
|
||||
class ReloadModelsTest < ActiveRecord::TestCase
|
||||
fixtures :pets
|
||||
|
||||
def test_has_one_with_reload
|
||||
pet = Pet.find_by_name('parrot')
|
||||
pet.owner = Owner.find_by_name('ashley')
|
||||
@@ -17,4 +19,4 @@ class ReloadModelsTest < ActiveRecord::TestCase
|
||||
pet.owner = Owner.find_by_name('ashley')
|
||||
assert_equal pet.owner, Owner.find_by_name('ashley')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
50
activerecord/test/cases/repair_helper.rb
Normal file
50
activerecord/test/cases/repair_helper.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
module ActiveRecord
|
||||
module Testing
|
||||
module RepairHelper
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
extend ClassMethods
|
||||
end
|
||||
end
|
||||
|
||||
module Toolbox
|
||||
def self.record_validations(*model_classes)
|
||||
model_classes.inject({}) do |repair, klass|
|
||||
repair[klass] ||= {}
|
||||
[:validate, :validate_on_create, :validate_on_update].each do |callback|
|
||||
the_callback = klass.instance_variable_get("@#{callback.to_s}_callbacks")
|
||||
repair[klass][callback] = (the_callback.nil? ? nil : the_callback.dup)
|
||||
end
|
||||
repair
|
||||
end
|
||||
end
|
||||
|
||||
def self.reset_validations(recorded)
|
||||
recorded.each do |klass, repairs|
|
||||
[:validate, :validate_on_create, :validate_on_update].each do |callback|
|
||||
klass.instance_variable_set("@#{callback.to_s}_callbacks", repairs[callback])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def repair_validations(*model_classes)
|
||||
setup do
|
||||
@validation_repairs = ActiveRecord::Testing::RepairHelper::Toolbox.record_validations(*model_classes)
|
||||
end
|
||||
teardown do
|
||||
ActiveRecord::Testing::RepairHelper::Toolbox.reset_validations(@validation_repairs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def repair_validations(*model_classes, &block)
|
||||
validation_repairs = ActiveRecord::Testing::RepairHelper::Toolbox.record_validations(*model_classes)
|
||||
return block.call
|
||||
ensure
|
||||
ActiveRecord::Testing::RepairHelper::Toolbox.reset_validations(validation_repairs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -506,7 +506,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase
|
||||
|
||||
# validates_length_of :is w/o mocha
|
||||
|
||||
def test_validates_length_of_within_finds_custom_model_key_translation
|
||||
def test_validates_length_of_is_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
@@ -515,7 +515,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_finds_global_default_translation
|
||||
def test_validates_length_of_is_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
@@ -525,7 +525,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase
|
||||
|
||||
# validates_uniqueness_of w/o mocha
|
||||
|
||||
def test_validates_length_of_within_finds_custom_model_key_translation
|
||||
def test_validates_length_of_is_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
@@ -534,7 +534,7 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_finds_global_default_translation
|
||||
def test_validates_length_of_is_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
|
||||
@@ -6,6 +6,8 @@ require 'models/person'
|
||||
require 'models/developer'
|
||||
require 'models/warehouse_thing'
|
||||
require 'models/guid'
|
||||
require 'models/owner'
|
||||
require 'models/pet'
|
||||
|
||||
# The following methods in Topic are used in test_conditional_validation_*
|
||||
class Topic
|
||||
@@ -31,10 +33,6 @@ class UniqueReply < Reply
|
||||
validates_uniqueness_of :content, :scope => 'parent_id'
|
||||
end
|
||||
|
||||
class PlagiarizedReply < Reply
|
||||
validates_acceptance_of :author_name
|
||||
end
|
||||
|
||||
class SillyUniqueReply < UniqueReply
|
||||
end
|
||||
|
||||
@@ -58,11 +56,9 @@ end
|
||||
class ValidationsTest < ActiveRecord::TestCase
|
||||
fixtures :topics, :developers, 'warehouse-things'
|
||||
|
||||
def setup
|
||||
Topic.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
Topic.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
Topic.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
end
|
||||
# Most of the tests mess with the validations of Topic, so lets repair it all the time.
|
||||
# Other classes we mess with will be dealt with in the specific tests
|
||||
repair_validations(Topic)
|
||||
|
||||
def test_single_field_validation
|
||||
r = Reply.new
|
||||
@@ -134,7 +130,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_exception_on_create_bang_with_block
|
||||
assert_raises(ActiveRecord::RecordInvalid) do
|
||||
Reply.create!({ "title" => "OK" }) do |r|
|
||||
@@ -142,7 +138,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_exception_on_create_bang_many_with_block
|
||||
assert_raises(ActiveRecord::RecordInvalid) do
|
||||
Reply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
|
||||
@@ -229,21 +225,16 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_each
|
||||
perform = true
|
||||
hits = 0
|
||||
Topic.validates_each(:title, :content, [:title, :content]) do |record, attr|
|
||||
if perform
|
||||
record.errors.add attr, 'gotcha'
|
||||
hits += 1
|
||||
end
|
||||
record.errors.add attr, 'gotcha'
|
||||
hits += 1
|
||||
end
|
||||
t = Topic.new("title" => "valid", "content" => "whatever")
|
||||
assert !t.save
|
||||
assert_equal 4, hits
|
||||
assert_equal %w(gotcha gotcha), t.errors.on(:title)
|
||||
assert_equal %w(gotcha gotcha), t.errors.on(:content)
|
||||
ensure
|
||||
perform = false
|
||||
end
|
||||
|
||||
def test_no_title_confirmation
|
||||
@@ -315,8 +306,12 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_acceptance_of_as_database_column
|
||||
reply = PlagiarizedReply.create("author_name" => "Dan Brown")
|
||||
assert_equal "Dan Brown", reply["author_name"]
|
||||
repair_validations(Reply) do
|
||||
Reply.validates_acceptance_of(:author_name)
|
||||
|
||||
reply = Reply.create("author_name" => "Dan Brown")
|
||||
assert_equal "Dan Brown", reply["author_name"]
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_acceptance_of_with_non_existant_table
|
||||
@@ -372,22 +367,24 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validate_uniqueness_with_scope
|
||||
Reply.validates_uniqueness_of(:content, :scope => "parent_id")
|
||||
repair_validations(Reply) do
|
||||
Reply.validates_uniqueness_of(:content, :scope => "parent_id")
|
||||
|
||||
t = Topic.create("title" => "I'm unique!")
|
||||
t = Topic.create("title" => "I'm unique!")
|
||||
|
||||
r1 = t.replies.create "title" => "r1", "content" => "hello world"
|
||||
assert r1.valid?, "Saving r1"
|
||||
r1 = t.replies.create "title" => "r1", "content" => "hello world"
|
||||
assert r1.valid?, "Saving r1"
|
||||
|
||||
r2 = t.replies.create "title" => "r2", "content" => "hello world"
|
||||
assert !r2.valid?, "Saving r2 first time"
|
||||
r2 = t.replies.create "title" => "r2", "content" => "hello world"
|
||||
assert !r2.valid?, "Saving r2 first time"
|
||||
|
||||
r2.content = "something else"
|
||||
assert r2.save, "Saving r2 second time"
|
||||
r2.content = "something else"
|
||||
assert r2.save, "Saving r2 second time"
|
||||
|
||||
t2 = Topic.create("title" => "I'm unique too!")
|
||||
r3 = t2.replies.create "title" => "r3", "content" => "hello world"
|
||||
assert r3.valid?, "Saving r3"
|
||||
t2 = Topic.create("title" => "I'm unique too!")
|
||||
r3 = t2.replies.create "title" => "r3", "content" => "hello world"
|
||||
assert r3.valid?, "Saving r3"
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_uniqueness_scoped_to_defining_class
|
||||
@@ -406,27 +403,29 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validate_uniqueness_with_scope_array
|
||||
Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id])
|
||||
repair_validations(Reply) do
|
||||
Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id])
|
||||
|
||||
t = Topic.create("title" => "The earth is actually flat!")
|
||||
t = Topic.create("title" => "The earth is actually flat!")
|
||||
|
||||
r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply"
|
||||
assert r1.valid?, "Saving r1"
|
||||
r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply"
|
||||
assert r1.valid?, "Saving r1"
|
||||
|
||||
r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
|
||||
assert !r2.valid?, "Saving r2. Double reply by same author."
|
||||
r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
|
||||
assert !r2.valid?, "Saving r2. Double reply by same author."
|
||||
|
||||
r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
|
||||
assert r2.save, "Saving r2 the second time."
|
||||
r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
|
||||
assert r2.save, "Saving r2 the second time."
|
||||
|
||||
r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
|
||||
assert !r3.valid?, "Saving r3"
|
||||
r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
|
||||
assert !r3.valid?, "Saving r3"
|
||||
|
||||
r3.author_name = "jj"
|
||||
assert r3.save, "Saving r3 the second time."
|
||||
r3.author_name = "jj"
|
||||
assert r3.save, "Saving r3 the second time."
|
||||
|
||||
r3.author_name = "jeremy"
|
||||
assert !r3.save, "Saving r3 the third time."
|
||||
r3.author_name = "jeremy"
|
||||
assert !r3.save, "Saving r3 the third time."
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_case_insensitive_uniqueness
|
||||
@@ -523,10 +522,12 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validate_uniqueness_with_columns_which_are_sql_keywords
|
||||
Guid.validates_uniqueness_of :key
|
||||
g = Guid.new
|
||||
g.key = "foo"
|
||||
assert_nothing_raised { !g.valid? }
|
||||
repair_validations(Guid) do
|
||||
Guid.validates_uniqueness_of :key
|
||||
g = Guid.new
|
||||
g.key = "foo"
|
||||
assert_nothing_raised { !g.valid? }
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_straight_inheritance_uniqueness
|
||||
@@ -648,10 +649,12 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_numericality_with_getter_method
|
||||
Developer.validates_numericality_of( :salary )
|
||||
developer = Developer.new("name" => "michael", "salary" => nil)
|
||||
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
|
||||
assert developer.valid?
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_numericality_of( :salary )
|
||||
developer = Developer.new("name" => "michael", "salary" => nil)
|
||||
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
|
||||
assert developer.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_length_of_with_allow_nil
|
||||
@@ -684,10 +687,12 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_numericality_with_allow_nil_and_getter_method
|
||||
Developer.validates_numericality_of( :salary, :allow_nil => true)
|
||||
developer = Developer.new("name" => "michael", "salary" => nil)
|
||||
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
|
||||
assert developer.valid?
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_numericality_of( :salary, :allow_nil => true)
|
||||
developer = Developer.new("name" => "michael", "salary" => nil)
|
||||
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
|
||||
assert developer.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_exclusion_of
|
||||
@@ -892,26 +897,30 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_size_of_association
|
||||
assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 }
|
||||
t = Topic.new('title' => 'noreplies', 'content' => 'whatever')
|
||||
assert !t.save
|
||||
assert t.errors.on(:replies)
|
||||
reply = t.replies.build('title' => 'areply', 'content' => 'whateveragain')
|
||||
assert t.valid?
|
||||
repair_validations(Owner) do
|
||||
assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
|
||||
o = Owner.new('name' => 'nopets')
|
||||
assert !o.save
|
||||
assert o.errors.on(:pets)
|
||||
pet = o.pets.build('name' => 'apet')
|
||||
assert o.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_size_of_association_using_within
|
||||
assert_nothing_raised { Topic.validates_size_of :replies, :within => 1..2 }
|
||||
t = Topic.new('title' => 'noreplies', 'content' => 'whatever')
|
||||
assert !t.save
|
||||
assert t.errors.on(:replies)
|
||||
repair_validations(Owner) do
|
||||
assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
|
||||
o = Owner.new('name' => 'nopets')
|
||||
assert !o.save
|
||||
assert o.errors.on(:pets)
|
||||
|
||||
reply = t.replies.build('title' => 'areply', 'content' => 'whateveragain')
|
||||
assert t.valid?
|
||||
pet = o.pets.build('name' => 'apet')
|
||||
assert o.valid?
|
||||
|
||||
2.times { t.replies.build('title' => 'areply', 'content' => 'whateveragain') }
|
||||
assert !t.save
|
||||
assert t.errors.on(:replies)
|
||||
2.times { o.pets.build('name' => 'apet') }
|
||||
assert !o.save
|
||||
assert o.errors.on(:pets)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_length_of_nasty_params
|
||||
@@ -1102,13 +1111,15 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_size_of_association_utf8
|
||||
with_kcode('UTF8') do
|
||||
assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 }
|
||||
t = Topic.new('title' => 'あいうえお', 'content' => 'かきくけこ')
|
||||
assert !t.save
|
||||
assert t.errors.on(:replies)
|
||||
t.replies.build('title' => 'あいうえお', 'content' => 'かきくけこ')
|
||||
assert t.valid?
|
||||
repair_validations(Owner) do
|
||||
with_kcode('UTF8') do
|
||||
assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
|
||||
o = Owner.new('name' => 'あいうえおかきくけこ')
|
||||
assert !o.save
|
||||
assert o.errors.on(:pets)
|
||||
o.pets.build('name' => 'あいうえおかきくけこ')
|
||||
assert o.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1127,14 +1138,16 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_associated_one
|
||||
Reply.validates_associated( :topic )
|
||||
Topic.validates_presence_of( :content )
|
||||
r = Reply.new("title" => "A reply", "content" => "with content!")
|
||||
r.topic = Topic.create("title" => "uhohuhoh")
|
||||
assert !r.valid?
|
||||
assert r.errors.on(:topic)
|
||||
r.topic.content = "non-empty"
|
||||
assert r.valid?
|
||||
repair_validations(Reply) do
|
||||
Reply.validates_associated( :topic )
|
||||
Topic.validates_presence_of( :content )
|
||||
r = Reply.new("title" => "A reply", "content" => "with content!")
|
||||
r.topic = Topic.create("title" => "uhohuhoh")
|
||||
assert !r.valid?
|
||||
assert r.errors.on(:topic)
|
||||
r.topic.content = "non-empty"
|
||||
assert r.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_block
|
||||
@@ -1158,85 +1171,105 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_acceptance_of_with_custom_error_using_quotes
|
||||
Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.salary = "0"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.salary = "0"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_confirmation_of_with_custom_error_using_quotes
|
||||
Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "John"
|
||||
d.name_confirmation = "Johnny"
|
||||
assert !d.valid?
|
||||
assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "John"
|
||||
d.name_confirmation = "Johnny"
|
||||
assert !d.valid?
|
||||
assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_format_of_with_custom_error_using_quotes
|
||||
Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = d.name_confirmation = "John 32"
|
||||
assert !d.valid?
|
||||
assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = d.name_confirmation = "John 32"
|
||||
assert !d.valid?
|
||||
assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_inclusion_of_with_custom_error_using_quotes
|
||||
Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.salary = "90,000"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.salary = "90,000"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_length_of_with_custom_too_long_using_quotes
|
||||
Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Jeffrey"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Jeffrey"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_length_of_with_custom_too_short_using_quotes
|
||||
Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Joe"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Joe"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_length_of_with_custom_message_using_quotes
|
||||
Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Joe"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Joe"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_presence_of_with_custom_message_using_quotes
|
||||
Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Joe"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent)
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "Joe"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_uniqueness_of_with_custom_message_using_quotes
|
||||
Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "David"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
d = Developer.new
|
||||
d.name = "David"
|
||||
assert !d.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_associated_with_custom_message_using_quotes
|
||||
Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
Topic.validates_presence_of :content
|
||||
r = Reply.create("title" => "A reply", "content" => "with content!")
|
||||
r.topic = Topic.create("title" => "uhohuhoh")
|
||||
assert !r.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic).last
|
||||
repair_validations(Reply) do
|
||||
Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
|
||||
Topic.validates_presence_of :content
|
||||
r = Reply.create("title" => "A reply", "content" => "with content!")
|
||||
r.topic = Topic.create("title" => "uhohuhoh")
|
||||
assert !r.valid?
|
||||
assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic)
|
||||
end
|
||||
end
|
||||
|
||||
def test_if_validation_using_method_true
|
||||
@@ -1346,13 +1379,15 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_validates_associated_missing
|
||||
Reply.validates_presence_of(:topic)
|
||||
r = Reply.create("title" => "A reply", "content" => "with content!")
|
||||
assert !r.valid?
|
||||
assert r.errors.on(:topic)
|
||||
repair_validations(Reply) do
|
||||
Reply.validates_presence_of(:topic)
|
||||
r = Reply.create("title" => "A reply", "content" => "with content!")
|
||||
assert !r.valid?
|
||||
assert r.errors.on(:topic)
|
||||
|
||||
r.topic = Topic.find :first
|
||||
assert r.valid?
|
||||
r.topic = Topic.find :first
|
||||
assert r.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_errors_to_xml
|
||||
@@ -1364,14 +1399,14 @@ class ValidationsTest < ActiveRecord::TestCase
|
||||
assert xml.include?("<error>Content Empty</error>")
|
||||
end
|
||||
|
||||
def test_validation_order
|
||||
Topic.validates_presence_of :title
|
||||
Topic.validates_length_of :title, :minimum => 2
|
||||
def test_validation_order
|
||||
Topic.validates_presence_of :title
|
||||
Topic.validates_length_of :title, :minimum => 2
|
||||
|
||||
t = Topic.new("title" => "")
|
||||
assert !t.valid?
|
||||
assert_equal "can't be blank", t.errors.on("title").first
|
||||
end
|
||||
t = Topic.new("title" => "")
|
||||
assert !t.valid?
|
||||
assert_equal "can't be blank", t.errors.on("title").first
|
||||
end
|
||||
|
||||
# previous implementation of validates_presence_of eval'd the
|
||||
# string with the wrong binding, this regression test is to
|
||||
@@ -1423,11 +1458,7 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
|
||||
JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
|
||||
INFINITY = [1.0/0.0]
|
||||
|
||||
def setup
|
||||
Topic.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
Topic.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
Topic.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
end
|
||||
repair_validations(Topic)
|
||||
|
||||
def test_default_validates_numericality_of
|
||||
Topic.validates_numericality_of :approved
|
||||
|
||||
@@ -31,6 +31,13 @@ class XmlSerializationTest < ActiveRecord::TestCase
|
||||
assert_match %r{<created_at}, @xml
|
||||
end
|
||||
|
||||
def test_should_allow_camelized_tags
|
||||
@xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true
|
||||
assert_match %r{^<XmlContact>}, @xml
|
||||
assert_match %r{</XmlContact>$}, @xml
|
||||
assert_match %r{<CreatedAt}, @xml
|
||||
end
|
||||
|
||||
def test_should_include_yielded_additions
|
||||
@xml = Contact.new.to_xml do |xml|
|
||||
xml.creator "David"
|
||||
|
||||
6
activerecord/test/fixtures/member_types.yml
vendored
Normal file
6
activerecord/test/fixtures/member_types.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
founding:
|
||||
id: 1
|
||||
name: Founding
|
||||
provisional:
|
||||
id: 2
|
||||
name: Provisional
|
||||
4
activerecord/test/fixtures/members.yml
vendored
4
activerecord/test/fixtures/members.yml
vendored
@@ -1,4 +1,6 @@
|
||||
groucho:
|
||||
name: Groucho Marx
|
||||
member_type_id: 1
|
||||
some_other_guy:
|
||||
name: Englebert Humperdink
|
||||
name: Englebert Humperdink
|
||||
member_type_id: 2
|
||||
|
||||
11
activerecord/test/fixtures/people.yml
vendored
11
activerecord/test/fixtures/people.yml
vendored
@@ -1,6 +1,15 @@
|
||||
michael:
|
||||
id: 1
|
||||
first_name: Michael
|
||||
primary_contact_id: 2
|
||||
gender: M
|
||||
david:
|
||||
id: 2
|
||||
first_name: David
|
||||
first_name: David
|
||||
primary_contact_id: 3
|
||||
gender: M
|
||||
susan:
|
||||
id: 3
|
||||
first_name: Susan
|
||||
primary_contact_id: 2
|
||||
gender: F
|
||||
@@ -80,6 +80,7 @@ class ExclusivelyDependentFirm < Company
|
||||
has_one :account, :foreign_key => "firm_id", :dependent => :delete
|
||||
has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'"
|
||||
has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.']
|
||||
has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'}
|
||||
end
|
||||
|
||||
class Client < Company
|
||||
|
||||
@@ -8,4 +8,5 @@ class Member < ActiveRecord::Base
|
||||
has_one :sponsor_club, :through => :sponsor
|
||||
has_one :member_detail
|
||||
has_one :organization, :through => :member_detail
|
||||
belongs_to :member_type
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
class MemberDetail < ActiveRecord::Base
|
||||
belongs_to :member
|
||||
belongs_to :organization
|
||||
has_one :member_type, :through => :member
|
||||
end
|
||||
|
||||
3
activerecord/test/models/member_type.rb
Normal file
3
activerecord/test/models/member_type.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class MemberType < ActiveRecord::Base
|
||||
has_many :members
|
||||
end
|
||||
@@ -7,4 +7,10 @@ class Person < ActiveRecord::Base
|
||||
has_many :jobs, :through => :references
|
||||
has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true]
|
||||
has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id'
|
||||
|
||||
belongs_to :primary_contact, :class_name => 'Person'
|
||||
has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'
|
||||
|
||||
named_scope :males, :conditions => { :gender => 'M' }
|
||||
named_scope :females, :conditions => { :gender => 'F' }
|
||||
end
|
||||
|
||||
@@ -195,6 +195,7 @@ ActiveRecord::Schema.define do
|
||||
|
||||
create_table :members, :force => true do |t|
|
||||
t.string :name
|
||||
t.integer :member_type_id
|
||||
end
|
||||
|
||||
create_table :member_details, :force => true do |t|
|
||||
@@ -210,6 +211,10 @@ ActiveRecord::Schema.define do
|
||||
t.string :type
|
||||
end
|
||||
|
||||
create_table :member_types, :force => true do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
||||
create_table :references, :force => true do |t|
|
||||
t.integer :person_id
|
||||
t.integer :job_id
|
||||
@@ -293,8 +298,10 @@ ActiveRecord::Schema.define do
|
||||
end
|
||||
|
||||
create_table :people, :force => true do |t|
|
||||
t.string :first_name, :null => false
|
||||
t.integer :lock_version, :null => false, :default => 0
|
||||
t.string :first_name, :null => false
|
||||
t.references :primary_contact
|
||||
t.string :gender, :limit => 1
|
||||
t.integer :lock_version, :null => false, :default => 0
|
||||
end
|
||||
|
||||
create_table :pets, :primary_key => :pet_id ,:force => true do |t|
|
||||
|
||||
Reference in New Issue
Block a user