mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Added support for associating unsaved objects #402 [Tim Bates]
Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 [Tim Bates] Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 [Tim Bates] Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 [Tim Bates] Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 [Tim Bates] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@417 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -1,5 +1,38 @@
|
||||
*SVN*
|
||||
|
||||
* Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition:
|
||||
|
||||
== Unsaved objects and associations
|
||||
|
||||
You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be
|
||||
aware of, mostly involving the saving of associated objects.
|
||||
|
||||
=== One-to-one associations
|
||||
|
||||
* Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in
|
||||
order to update their primary keys - except if the parent object is unsaved (new_record? == true).
|
||||
* If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment
|
||||
is cancelled.
|
||||
* If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below).
|
||||
* Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does
|
||||
not save the parent either.
|
||||
|
||||
=== Collections
|
||||
|
||||
* Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object
|
||||
(the owner of the collection) is not yet stored in the database.
|
||||
* If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false.
|
||||
* You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
|
||||
* All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
|
||||
|
||||
* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 [Tim Bates]
|
||||
|
||||
* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 [Tim Bates]
|
||||
|
||||
* Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 [Tim Bates]
|
||||
|
||||
* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 [Tim Bates]
|
||||
|
||||
* Fixed binary support for PostgreSQL #444 [alex@byzantine.no]
|
||||
|
||||
* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
require 'active_record/associations/association_proxy'
|
||||
require 'active_record/associations/association_collection'
|
||||
require 'active_record/associations/belongs_to_association'
|
||||
require 'active_record/associations/has_one_association'
|
||||
require 'active_record/associations/has_many_association'
|
||||
require 'active_record/associations/has_and_belongs_to_many_association'
|
||||
require 'active_record/deprecated_associations'
|
||||
@@ -30,9 +33,9 @@ module ActiveRecord
|
||||
# end
|
||||
#
|
||||
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
|
||||
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
|
||||
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
|
||||
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
|
||||
# <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
|
||||
# <tt>Project#project_manager.build, Project#project_manager.create</tt>
|
||||
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
|
||||
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
|
||||
# <tt>Project#milestones.build, Project#milestones.create</tt>
|
||||
@@ -71,6 +74,29 @@ module ActiveRecord
|
||||
# PRIMARY KEY (id)
|
||||
# )
|
||||
#
|
||||
# == Unsaved objects and associations
|
||||
#
|
||||
# You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be
|
||||
# aware of, mostly involving the saving of associated objects.
|
||||
#
|
||||
# === One-to-one associations
|
||||
#
|
||||
# * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in
|
||||
# order to update their primary keys - except if the parent object is unsaved (new_record? == true).
|
||||
# * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment
|
||||
# is cancelled.
|
||||
# * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below).
|
||||
# * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does
|
||||
# not save the parent either.
|
||||
#
|
||||
# === Collections
|
||||
#
|
||||
# * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object
|
||||
# (the owner of the collection) is not yet stored in the database.
|
||||
# * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false.
|
||||
# * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
|
||||
# * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
|
||||
#
|
||||
# == Caching
|
||||
#
|
||||
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
|
||||
@@ -124,7 +150,7 @@ module ActiveRecord
|
||||
module ClassMethods
|
||||
# Adds the following methods for retrieval and query of collections of associated objects.
|
||||
# +collection+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
|
||||
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
|
||||
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
|
||||
# An empty array is returned if none are found.
|
||||
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
||||
@@ -200,18 +226,9 @@ module ActiveRecord
|
||||
module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
|
||||
end
|
||||
|
||||
define_method(association_name) do |*params|
|
||||
force_reload = params.first unless params.empty?
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
if association.nil?
|
||||
association = HasManyAssociation.new(self,
|
||||
association_name, association_class_name,
|
||||
association_class_primary_key_name, options)
|
||||
instance_variable_set("@#{association_name}", association)
|
||||
end
|
||||
association.reload if force_reload
|
||||
association
|
||||
end
|
||||
add_multiple_associated_save_callbacks(association_name)
|
||||
|
||||
association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
|
||||
|
||||
# deprecated api
|
||||
deprecated_collection_count_method(association_name)
|
||||
@@ -220,31 +237,28 @@ module ActiveRecord
|
||||
deprecated_has_collection_method(association_name)
|
||||
deprecated_find_in_collection_method(association_name)
|
||||
deprecated_find_all_in_collection_method(association_name)
|
||||
deprecated_create_method(association_name)
|
||||
deprecated_build_method(association_name)
|
||||
deprecated_collection_create_method(association_name)
|
||||
deprecated_collection_build_method(association_name)
|
||||
end
|
||||
|
||||
# Adds the following methods for retrieval and query of a single associated object.
|
||||
# +association+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
|
||||
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
|
||||
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
|
||||
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
|
||||
# and saves the associate object.
|
||||
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
|
||||
# same id as the associated object.
|
||||
# * <tt>association.nil?</tt> - returns true if there is no associated object.
|
||||
# * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
||||
# * <tt>association.build(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
|
||||
# * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
||||
# * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
|
||||
#
|
||||
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
||||
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
|
||||
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
||||
# * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
|
||||
# * <tt>Account#beneficiary.nil?</tt>
|
||||
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
||||
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
||||
# * <tt>Account#beneficiary.build</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
||||
# * <tt>Account#beneficiary.create</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
#
|
||||
# Options are:
|
||||
@@ -265,35 +279,53 @@ module ActiveRecord
|
||||
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
|
||||
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
|
||||
def has_one(association_id, options = {})
|
||||
options.merge!({ :remote => true })
|
||||
belongs_to(association_id, options)
|
||||
validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
|
||||
|
||||
association_name, association_class_name, class_primary_key_name =
|
||||
association_name, association_class_name, association_class_primary_key_name =
|
||||
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
|
||||
|
||||
require_association_class(association_class_name)
|
||||
|
||||
has_one_writer_method(association_name, association_class_name, class_primary_key_name)
|
||||
build_method("build_", association_name, association_class_name, class_primary_key_name)
|
||||
create_method("create_", association_name, association_class_name, class_primary_key_name)
|
||||
module_eval do
|
||||
after_save <<-EOF
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
if (true or @new_record_before_save) and association.respond_to?(:loaded?) and not association.nil?
|
||||
association["#{association_class_primary_key_name}"] = id
|
||||
association.save(true)
|
||||
association.send(:construct_sql)
|
||||
end
|
||||
EOF
|
||||
end
|
||||
|
||||
association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
|
||||
|
||||
module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
|
||||
module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'" if options[:dependent]
|
||||
|
||||
# deprecated api
|
||||
deprecated_has_association_method(association_name)
|
||||
deprecated_build_method("build_", association_name, association_class_name, association_class_primary_key_name)
|
||||
deprecated_create_method("create_", association_name, association_class_name, association_class_primary_key_name)
|
||||
deprecated_association_comparison_method(association_name, association_class_name)
|
||||
end
|
||||
|
||||
# Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
|
||||
# +association+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
|
||||
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
|
||||
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
|
||||
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
||||
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
|
||||
# same id as the associated object.
|
||||
# * <tt>association.nil?</tt> - returns true if there is no associated object.
|
||||
# * <tt>association.build(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
|
||||
# * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
|
||||
#
|
||||
# Example: A Post class declares <tt>has_one :author</tt>, which will add:
|
||||
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
||||
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
||||
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
||||
# * <tt>Post#author.nil?</tt>
|
||||
# * <tt>Post#author.build</tt> (similar to <tt>Author.new("post_id" => id)</tt>)
|
||||
# * <tt>Post#author.create</tt> (similar to <tt>b = Author.new("post_id" => id); b.save; b</tt>)
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
#
|
||||
# Options are:
|
||||
@@ -317,46 +349,45 @@ module ActiveRecord
|
||||
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
|
||||
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
||||
# :conditions => 'discounts > #{payments_count}'
|
||||
def belongs_to(association_id, options = {})
|
||||
validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
|
||||
def belongs_to(association_id, options = {})
|
||||
validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
|
||||
|
||||
association_name, association_class_name, class_primary_key_name =
|
||||
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
|
||||
association_name, association_class_name, class_primary_key_name =
|
||||
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
|
||||
|
||||
require_association_class(association_class_name)
|
||||
require_association_class(association_class_name)
|
||||
|
||||
association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
|
||||
association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
|
||||
|
||||
if options[:remote]
|
||||
association_finder = <<-"end_eval"
|
||||
#{association_class_name}.find_first(
|
||||
"#{class_primary_key_name} = \#{quoted_id}#{options[:conditions] ? " AND " + options[:conditions] : ""}",
|
||||
#{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
|
||||
)
|
||||
end_eval
|
||||
else
|
||||
association_finder = options[:conditions] ?
|
||||
"#{association_class_name}.find_on_conditions(read_attribute(\"#{association_class_primary_key_name}\"), \"#{options[:conditions]}\")" :
|
||||
"#{association_class_name}.find(read_attribute(\"#{association_class_primary_key_name}\"))"
|
||||
end
|
||||
association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
|
||||
|
||||
has_association_method(association_name)
|
||||
association_reader_method(association_name, association_finder)
|
||||
belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
|
||||
association_comparison_method(association_name, association_class_name)
|
||||
|
||||
if options[:counter_cache]
|
||||
module_eval(
|
||||
"after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
|
||||
" if has_#{association_name}?'"
|
||||
)
|
||||
|
||||
module_eval(
|
||||
"before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
|
||||
" if has_#{association_name}?'"
|
||||
)
|
||||
end
|
||||
module_eval do
|
||||
before_save <<-EOF
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
if association.respond_to?(:loaded?) and not association.nil? and association.new_record?
|
||||
association.save(true)
|
||||
self["#{association_class_primary_key_name}"] = association.id
|
||||
association.send(:construct_sql)
|
||||
end
|
||||
EOF
|
||||
end
|
||||
|
||||
if options[:counter_cache]
|
||||
module_eval(
|
||||
"after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
|
||||
" unless #{association_name}.nil?'"
|
||||
)
|
||||
|
||||
module_eval(
|
||||
"before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
|
||||
" unless #{association_name}.nil?'"
|
||||
)
|
||||
end
|
||||
|
||||
# deprecated api
|
||||
deprecated_has_association_method(association_name)
|
||||
deprecated_association_comparison_method(association_name, association_class_name)
|
||||
end
|
||||
|
||||
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
|
||||
# an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
|
||||
@@ -371,7 +402,7 @@ module ActiveRecord
|
||||
#
|
||||
# Adds the following methods for retrieval and query.
|
||||
# +collection+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
|
||||
# <tt>has_and_belongs_to_many :categories</tt> would add among others +categories.empty?+.
|
||||
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
|
||||
# An empty array is returned if none is found.
|
||||
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
|
||||
@@ -385,10 +416,13 @@ module ActiveRecord
|
||||
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
|
||||
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
|
||||
# * <tt>collection.size</tt> - returns the number of associated objects.
|
||||
# * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
|
||||
# meets the condition that it has to be associated with this object.
|
||||
#
|
||||
# Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
|
||||
# * <tt>Developer#projects</tt>
|
||||
# * <tt>Developer#projects<<</tt>
|
||||
# * <tt>Developer#projects.push_with_attributes</tt>
|
||||
# * <tt>Developer#projects.delete</tt>
|
||||
# * <tt>Developer#projects.clear</tt>
|
||||
# * <tt>Developer#projects.empty?</tt>
|
||||
@@ -431,23 +465,13 @@ module ActiveRecord
|
||||
|
||||
require_association_class(association_class_name)
|
||||
|
||||
join_table = options[:join_table] ||
|
||||
join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
|
||||
|
||||
define_method(association_name) do |*params|
|
||||
force_reload = params.first unless params.empty?
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
if association.nil?
|
||||
association = HasAndBelongsToManyAssociation.new(self,
|
||||
association_name, association_class_name,
|
||||
association_class_primary_key_name, join_table, options)
|
||||
instance_variable_set("@#{association_name}", association)
|
||||
end
|
||||
association.reload if force_reload
|
||||
association
|
||||
end
|
||||
options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
|
||||
|
||||
before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
|
||||
add_multiple_associated_save_callbacks(association_name)
|
||||
|
||||
association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation)
|
||||
|
||||
before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
|
||||
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
|
||||
|
||||
# deprecated api
|
||||
@@ -487,94 +511,70 @@ module ActiveRecord
|
||||
return association_id.id2name, association_class_name, primary_key_name
|
||||
end
|
||||
|
||||
def association_comparison_method(association_name, association_class_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{association_name}?(comparison_object, force_reload = false)
|
||||
if comparison_object.kind_of?(#{association_class_name})
|
||||
#{association_name}(force_reload) == comparison_object
|
||||
else
|
||||
raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
|
||||
end
|
||||
def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
|
||||
define_method(association_name) do |*params|
|
||||
force_reload = params.first unless params.empty?
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
unless association.respond_to?(:loaded?)
|
||||
association = association_proxy_class.new(self,
|
||||
association_name, association_class_name,
|
||||
association_class_primary_key_name, options)
|
||||
instance_variable_set("@#{association_name}", association)
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
association.reload if force_reload
|
||||
association
|
||||
end
|
||||
|
||||
def association_reader_method(association_name, association_finder)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{association_name}(force_reload = false)
|
||||
if @#{association_name}.nil? || force_reload
|
||||
begin
|
||||
@#{association_name} = #{association_finder}
|
||||
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
return @#{association_name}
|
||||
define_method("#{association_name}=") do |new_value|
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
unless association.respond_to?(:loaded?)
|
||||
association = association_proxy_class.new(self,
|
||||
association_name, association_class_name,
|
||||
association_class_primary_key_name, options)
|
||||
instance_variable_set("@#{association_name}", association)
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{association_name}=(association)
|
||||
if association.nil?
|
||||
@#{association_name}.#{class_primary_key_name} = nil
|
||||
@#{association_name}.save(false)
|
||||
@#{association_name} = nil
|
||||
else
|
||||
raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
|
||||
association.#{class_primary_key_name} = id
|
||||
association.save(false)
|
||||
@#{association_name} = association
|
||||
end
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{association_name}=(association)
|
||||
if association.nil?
|
||||
@#{association_name} = self.#{association_class_primary_key_name} = nil
|
||||
else
|
||||
raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
|
||||
@#{association_name} = association
|
||||
self.#{association_class_primary_key_name} = association.id
|
||||
end
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def has_association_method(association_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def has_#{association_name}?(force_reload = false)
|
||||
!#{association_name}(force_reload).nil?
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{method_prefix + collection_name}(attributes = {})
|
||||
association = #{collection_class_name}.new
|
||||
association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
|
||||
association
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{method_prefix + collection_name}(attributes = nil)
|
||||
#{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
|
||||
end
|
||||
end_eval
|
||||
association.replace(new_value)
|
||||
association
|
||||
end
|
||||
end
|
||||
|
||||
def require_association_class(class_name)
|
||||
require_association(Inflector.underscore(class_name)) if class_name
|
||||
end
|
||||
|
||||
def add_multiple_associated_save_callbacks(association_name)
|
||||
module_eval do
|
||||
before_save <<-end_eval
|
||||
@new_record_before_save = new_record?
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
if association.respond_to?(:loaded?)
|
||||
if new_record?
|
||||
records_to_save = association
|
||||
else
|
||||
records_to_save = association.select{ |record| record.new_record? }
|
||||
end
|
||||
records_to_save.inject(true) do |result,record|
|
||||
result &&= record.valid?
|
||||
end
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
module_eval do
|
||||
after_save <<-end_eval
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
if association.respond_to?(:loaded?)
|
||||
if @new_record_before_save
|
||||
records_to_save = association
|
||||
else
|
||||
records_to_save = association.select{ |record| record.new_record? }
|
||||
end
|
||||
records_to_save.each{ |record| association.send(:insert_record, record) }
|
||||
association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,51 +1,34 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class AssociationCollection #:nodoc:
|
||||
alias_method :proxy_respond_to?, :respond_to?
|
||||
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
|
||||
|
||||
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
@owner = owner
|
||||
@options = options
|
||||
@association_name = association_name
|
||||
@association_class = eval(association_class_name)
|
||||
@association_class_primary_key_name = association_class_primary_key_name
|
||||
end
|
||||
|
||||
def method_missing(symbol, *args, &block)
|
||||
load_collection
|
||||
@collection.send(symbol, *args, &block)
|
||||
end
|
||||
|
||||
class AssociationCollection < AssociationProxy #:nodoc:
|
||||
def to_ary
|
||||
load_collection
|
||||
@collection.to_ary
|
||||
load_target
|
||||
@target.to_ary
|
||||
end
|
||||
|
||||
def respond_to?(symbol, include_priv = false)
|
||||
proxy_respond_to?(symbol, include_priv) || [].respond_to?(symbol, include_priv)
|
||||
def reset
|
||||
@target = []
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
def loaded?
|
||||
!@collection.nil?
|
||||
end
|
||||
|
||||
def reload
|
||||
@collection = nil
|
||||
reset
|
||||
end
|
||||
|
||||
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
||||
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
||||
def <<(*records)
|
||||
result = true
|
||||
load_target
|
||||
@owner.transaction do
|
||||
flatten_deeper(records).each do |record|
|
||||
raise_on_type_mismatch(record)
|
||||
insert_record(record)
|
||||
@collection << record if loaded?
|
||||
result &&= insert_record(record) unless @owner.new_record?
|
||||
@target << record
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
result and self
|
||||
end
|
||||
|
||||
alias_method :push, :<<
|
||||
@@ -54,11 +37,13 @@ module ActiveRecord
|
||||
# Remove +records+ from this association. Does not destroy +records+.
|
||||
def delete(*records)
|
||||
records = flatten_deeper(records)
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
records.reject! { |record| @target.delete(record) if record.new_record? }
|
||||
return if records.empty?
|
||||
|
||||
@owner.transaction do
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
delete_records(records)
|
||||
records.each { |record| @collection.delete(record) } if loaded?
|
||||
records.each { |record| @target.delete(record) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,20 +52,27 @@ module ActiveRecord
|
||||
each { |record| record.destroy }
|
||||
end
|
||||
|
||||
@collection = []
|
||||
@target = []
|
||||
end
|
||||
|
||||
def create(attributes = {})
|
||||
# Can't use Base.create since the foreign key may be a protected attribute.
|
||||
record = build(attributes)
|
||||
record.save unless @owner.new_record?
|
||||
record
|
||||
end
|
||||
|
||||
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
||||
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
|
||||
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
|
||||
def size
|
||||
if loaded? then @collection.size else count_records end
|
||||
if loaded? then @target.size else count_records end
|
||||
end
|
||||
|
||||
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
|
||||
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
|
||||
def length
|
||||
load_collection.size
|
||||
load_target.size
|
||||
end
|
||||
|
||||
def empty?
|
||||
@@ -91,11 +83,14 @@ module ActiveRecord
|
||||
collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
|
||||
end
|
||||
|
||||
protected
|
||||
def loaded?
|
||||
not @collection.nil?
|
||||
end
|
||||
def replace(other_array)
|
||||
other_array.each{ |val| raise_on_type_mismatch(val) }
|
||||
|
||||
@target = other_array
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
protected
|
||||
def quoted_record_ids(records)
|
||||
records.map { |record| record.quoted_id }.join(',')
|
||||
end
|
||||
@@ -117,22 +112,14 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
private
|
||||
def load_collection
|
||||
if loaded?
|
||||
@collection
|
||||
else
|
||||
begin
|
||||
@collection = find_all_records
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@collection = []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def raise_on_type_mismatch(record)
|
||||
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
|
||||
end
|
||||
|
||||
def target_obsolete?
|
||||
false
|
||||
end
|
||||
|
||||
# Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
|
||||
def flatten_deeper(array)
|
||||
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class AssociationProxy #:nodoc:
|
||||
alias_method :proxy_respond_to?, :respond_to?
|
||||
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^send)/ }
|
||||
|
||||
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
@owner = owner
|
||||
@options = options
|
||||
@association_name = association_name
|
||||
@association_class = eval(association_class_name)
|
||||
@association_class_primary_key_name = association_class_primary_key_name
|
||||
|
||||
reset
|
||||
end
|
||||
|
||||
def method_missing(symbol, *args, &block)
|
||||
load_target
|
||||
@target.send(symbol, *args, &block)
|
||||
end
|
||||
|
||||
def respond_to?(symbol, include_priv = false)
|
||||
load_target
|
||||
proxy_respond_to?(symbol, include_priv) || @target.respond_to?(symbol, include_priv)
|
||||
end
|
||||
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
private
|
||||
def load_target
|
||||
unless @owner.new_record?
|
||||
begin
|
||||
@target = find_target if not loaded?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
reset
|
||||
end
|
||||
end
|
||||
@loaded = true
|
||||
@target
|
||||
end
|
||||
|
||||
def raise_on_type_mismatch(record)
|
||||
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class BelongsToAssociation < AssociationProxy #:nodoc:
|
||||
|
||||
def reset
|
||||
@target = nil
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
def reload
|
||||
reset
|
||||
load_target
|
||||
end
|
||||
|
||||
def create(attributes = {})
|
||||
record = build(attributes)
|
||||
record.save
|
||||
record
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
record = @association_class.new(attributes)
|
||||
replace(record, true)
|
||||
record
|
||||
end
|
||||
|
||||
def replace(obj, dont_save = false)
|
||||
if obj.nil?
|
||||
@target = @owner[@association_class_primary_key_name] = nil
|
||||
else
|
||||
raise_on_type_mismatch(obj) unless obj.nil?
|
||||
|
||||
@target = obj
|
||||
@owner[@association_class_primary_key_name] = obj.id unless obj.new_record?
|
||||
end
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
# Ugly workaround - .nil? is done in C and the method_missing trick doesn't work when we pretend to be nil
|
||||
def nil?
|
||||
load_target
|
||||
@target.nil?
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
if @options[:conditions]
|
||||
@association_class.find_on_conditions(@owner[@association_class_primary_key_name], @options[:conditions])
|
||||
else
|
||||
@association_class.find(@owner[@association_class_primary_key_name])
|
||||
end
|
||||
end
|
||||
|
||||
def target_obsolete?
|
||||
@owner[@association_class_primary_key_name] != @target.id
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
# no sql to construct
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class NilClass #:nodoc:
|
||||
# Ugly workaround - nil comparison is usually done in C and so a proxy object pretending to be nil doesn't work.
|
||||
def ==(other)
|
||||
other.nil?
|
||||
end
|
||||
end
|
||||
@@ -1,26 +1,27 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
|
||||
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
|
||||
super(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
super
|
||||
|
||||
@association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
|
||||
association_table_name = options[:table_name] || @association_class.table_name
|
||||
@join_table = join_table
|
||||
@association_table_name = options[:table_name] || @association_class.table_name
|
||||
@join_table = options[:join_table]
|
||||
@order = options[:order] || "t.#{@association_class.primary_key}"
|
||||
|
||||
interpolate_sql_options!(options, :finder_sql, :delete_sql)
|
||||
@finder_sql = options[:finder_sql] ||
|
||||
"SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
|
||||
"WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
|
||||
"j.#{association_class_primary_key_name} = #{@owner.quoted_id} " +
|
||||
(options[:conditions] ? " AND " + interpolate_sql(options[:conditions]) : "") + " " +
|
||||
"ORDER BY #{@order}"
|
||||
construct_sql
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
load_target
|
||||
record = @association_class.new(attributes)
|
||||
@target << record
|
||||
record
|
||||
end
|
||||
|
||||
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
||||
def clear
|
||||
return self if size == 0 # forces load_collection if hasn't happened already
|
||||
return self if size == 0 # forces load_target if hasn't happened already
|
||||
|
||||
if sql = @options[:delete_sql]
|
||||
each { |record| @owner.connection.execute(sql) }
|
||||
@@ -34,12 +35,12 @@ module ActiveRecord
|
||||
@owner.connection.execute(sql)
|
||||
end
|
||||
|
||||
@collection = []
|
||||
@target = []
|
||||
self
|
||||
end
|
||||
|
||||
def find_first
|
||||
load_collection.first
|
||||
load_target.first
|
||||
end
|
||||
|
||||
def find(*args)
|
||||
@@ -56,16 +57,16 @@ module ActiveRecord
|
||||
elsif @options[:finder_sql]
|
||||
if ids.size == 1
|
||||
id = ids.first
|
||||
record = load_collection.detect { |record| id == record.id }
|
||||
record = load_target.detect { |record| id == record.id }
|
||||
expects_array? ? [record] : record
|
||||
else
|
||||
load_collection.select { |record| ids.include?(record.id) }
|
||||
load_target.select { |record| ids.include?(record.id) }
|
||||
end
|
||||
|
||||
# Otherwise, construct a query.
|
||||
else
|
||||
ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
|
||||
records = find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
|
||||
records = find_target(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
|
||||
if records.size == ids.size
|
||||
if ids.size == 1 and !expects_array
|
||||
records.first
|
||||
@@ -82,7 +83,7 @@ module ActiveRecord
|
||||
raise_on_type_mismatch(record)
|
||||
insert_record_with_join_attributes(record, join_attributes)
|
||||
join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
|
||||
@collection << record if loaded?
|
||||
@target << record
|
||||
self
|
||||
end
|
||||
|
||||
@@ -93,16 +94,17 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
protected
|
||||
def find_all_records(sql = @finder_sql)
|
||||
def find_target(sql = @finder_sql)
|
||||
records = @association_class.find_by_sql(sql)
|
||||
@options[:uniq] ? uniq(records) : records
|
||||
end
|
||||
|
||||
def count_records
|
||||
load_collection.size
|
||||
load_target.size
|
||||
end
|
||||
|
||||
def insert_record(record)
|
||||
return false unless record.save
|
||||
if @options[:insert_sql]
|
||||
@owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
|
||||
else
|
||||
@@ -110,6 +112,7 @@ module ActiveRecord
|
||||
"VALUES (#{@owner.quoted_id},#{record.quoted_id})"
|
||||
@owner.connection.execute(sql)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def insert_record_with_join_attributes(record, join_attributes)
|
||||
@@ -129,6 +132,16 @@ module ActiveRecord
|
||||
@owner.connection.execute(sql)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
interpolate_sql_options!(@options, :finder_sql, :delete_sql)
|
||||
@finder_sql = @options[:finder_sql] ||
|
||||
"SELECT t.*, j.* FROM #{@association_table_name} t, #{@join_table} j " +
|
||||
"WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
|
||||
"j.#{@association_class_primary_key_name} = #{@owner.quoted_id} " +
|
||||
(@options[:conditions] ? " AND " + interpolate_sql(@options[:conditions]) : "") + " " +
|
||||
"ORDER BY #{@order}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,37 +2,17 @@ module ActiveRecord
|
||||
module Associations
|
||||
class HasManyAssociation < AssociationCollection #:nodoc:
|
||||
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
super(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
super
|
||||
@conditions = sanitize_sql(options[:conditions])
|
||||
|
||||
if options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(options[:finder_sql])
|
||||
else
|
||||
@finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
|
||||
@finder_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
|
||||
end
|
||||
|
||||
if options[:counter_sql]
|
||||
@counter_sql = interpolate_sql(options[:counter_sql])
|
||||
elsif options[:finder_sql]
|
||||
options[:counter_sql] = options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
|
||||
@counter_sql = interpolate_sql(options[:counter_sql])
|
||||
else
|
||||
@counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
|
||||
end
|
||||
end
|
||||
|
||||
def create(attributes = {})
|
||||
# Can't use Base.create since the foreign key may be a protected attribute.
|
||||
record = build(attributes)
|
||||
record.save
|
||||
@collection << record if loaded?
|
||||
record
|
||||
construct_sql
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
load_target
|
||||
record = @association_class.new(attributes)
|
||||
record[@association_class_primary_key_name] = @owner.id
|
||||
record[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
|
||||
@target << record
|
||||
record
|
||||
end
|
||||
|
||||
@@ -77,10 +57,10 @@ module ActiveRecord
|
||||
elsif @options[:finder_sql]
|
||||
if ids.size == 1
|
||||
id = ids.first
|
||||
record = load_collection.detect { |record| id == record.id }
|
||||
record = load_target.detect { |record| id == record.id }
|
||||
expects_array? ? [record] : record
|
||||
else
|
||||
load_collection.select { |record| ids.include?(record.id) }
|
||||
load_target.select { |record| ids.include?(record.id) }
|
||||
end
|
||||
|
||||
# Otherwise, delegate to association class with conditions.
|
||||
@@ -94,12 +74,12 @@ module ActiveRecord
|
||||
# method calls may be chained.
|
||||
def clear
|
||||
@association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = #{@owner.quoted_id}")
|
||||
@collection = []
|
||||
@target = []
|
||||
self
|
||||
end
|
||||
|
||||
protected
|
||||
def find_all_records
|
||||
def find_target
|
||||
find_all
|
||||
end
|
||||
|
||||
@@ -122,7 +102,8 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def insert_record(record)
|
||||
record.update_attribute(@association_class_primary_key_name, @owner.id)
|
||||
record[@association_class_primary_key_name] = @owner.id
|
||||
record.save
|
||||
end
|
||||
|
||||
def delete_records(records)
|
||||
@@ -132,6 +113,29 @@ module ActiveRecord
|
||||
"#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
|
||||
)
|
||||
end
|
||||
|
||||
def target_obsolete?
|
||||
false
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
if @options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@options[:finder_sql])
|
||||
else
|
||||
@finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
|
||||
@finder_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
|
||||
end
|
||||
|
||||
if @options[:counter_sql]
|
||||
@counter_sql = interpolate_sql(@options[:counter_sql])
|
||||
elsif @options[:finder_sql]
|
||||
@options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
|
||||
@counter_sql = interpolate_sql(@options[:counter_sql])
|
||||
else
|
||||
@counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
|
||||
@counter_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class HasOneAssociation < BelongsToAssociation #:nodoc:
|
||||
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
|
||||
super
|
||||
|
||||
construct_sql
|
||||
end
|
||||
|
||||
def replace(obj, dont_save = false)
|
||||
load_target
|
||||
unless @target.nil?
|
||||
@target[@association_class_primary_key_name] = nil
|
||||
@target.save unless @owner.new_record?
|
||||
end
|
||||
|
||||
if obj.nil?
|
||||
@target = nil
|
||||
else
|
||||
raise_on_type_mismatch(obj)
|
||||
|
||||
obj[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
|
||||
@target = obj
|
||||
end
|
||||
|
||||
@loaded = true
|
||||
unless @owner.new_record? or obj.nil? or dont_save
|
||||
return (obj.save ? obj : false)
|
||||
else
|
||||
return obj
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
@association_class.find_first(@finder_sql, @options[:order])
|
||||
end
|
||||
|
||||
def target_obsolete?
|
||||
false
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
@finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@options[:conditions] ? " AND " + @options[:conditions] : ""}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -789,7 +789,6 @@ module ActiveRecord #:nodoc:
|
||||
# * A record does exist: Updates the record with values matching those of the object attributes.
|
||||
def save
|
||||
create_or_update
|
||||
return true
|
||||
end
|
||||
|
||||
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
||||
@@ -928,9 +927,9 @@ module ActiveRecord #:nodoc:
|
||||
self.class.columns_hash[name.to_s]
|
||||
end
|
||||
|
||||
# Returns true if the +comparison_object+ is of the same type and has the same id.
|
||||
# Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
|
||||
def ==(comparison_object)
|
||||
comparison_object.instance_of?(self.class) && comparison_object.id == id
|
||||
comparison_object.equal?(self) or (comparison_object.instance_of?(self.class) and comparison_object.id == id)
|
||||
end
|
||||
|
||||
# Delegates to ==
|
||||
@@ -956,6 +955,7 @@ module ActiveRecord #:nodoc:
|
||||
private
|
||||
def create_or_update
|
||||
if new_record? then create else update end
|
||||
return true
|
||||
end
|
||||
|
||||
# Updates the associated record with values matching those of the instant attributes.
|
||||
|
||||
@@ -153,6 +153,12 @@ module ActiveRecord
|
||||
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
|
||||
# after_initialize can only be declared using an explicit implementation. So using the inheritable callback queue for after_find and
|
||||
# after_initialize won't work.
|
||||
#
|
||||
# == Cancelling callbacks
|
||||
#
|
||||
# If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns
|
||||
# false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
|
||||
# defined as methods on the model, which are called last.
|
||||
module Callbacks
|
||||
CALLBACKS = %w(
|
||||
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
|
||||
@@ -227,7 +233,7 @@ module ActiveRecord
|
||||
# Is called _after_ Base.save (regardless of whether it's a create or update save).
|
||||
def after_save() end
|
||||
def create_or_update_with_callbacks #:nodoc:
|
||||
callback(:before_save)
|
||||
return false if callback(:before_save) == false
|
||||
result = create_or_update_without_callbacks
|
||||
callback(:after_save)
|
||||
result
|
||||
@@ -239,7 +245,7 @@ module ActiveRecord
|
||||
# Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
|
||||
def after_create() end
|
||||
def create_with_callbacks #:nodoc:
|
||||
callback(:before_create)
|
||||
return false if callback(:before_create) == false
|
||||
result = create_without_callbacks
|
||||
callback(:after_create)
|
||||
result
|
||||
@@ -252,7 +258,7 @@ module ActiveRecord
|
||||
def after_update() end
|
||||
|
||||
def update_with_callbacks #:nodoc:
|
||||
callback(:before_update)
|
||||
return false if callback(:before_update) == false
|
||||
result = update_without_callbacks
|
||||
callback(:after_update)
|
||||
result
|
||||
@@ -281,8 +287,9 @@ module ActiveRecord
|
||||
def after_validation_on_update() end
|
||||
|
||||
def valid_with_callbacks #:nodoc:
|
||||
callback(:before_validation)
|
||||
if new_record? then callback(:before_validation_on_create) else callback(:before_validation_on_update) end
|
||||
return false if callback(:before_validation) == false
|
||||
if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
|
||||
return false if result == false
|
||||
|
||||
result = valid_without_callbacks
|
||||
|
||||
@@ -298,7 +305,7 @@ module ActiveRecord
|
||||
# Is called _after_ Base.destroy (and all the attributes have been frozen).
|
||||
def after_destroy() end
|
||||
def destroy_with_callbacks #:nodoc:
|
||||
callback(:before_destroy)
|
||||
return false if callback(:before_destroy) == false
|
||||
result = destroy_without_callbacks
|
||||
callback(:after_destroy)
|
||||
result
|
||||
@@ -307,7 +314,7 @@ module ActiveRecord
|
||||
private
|
||||
def callback(method)
|
||||
callbacks_for(method).each do |callback|
|
||||
case callback
|
||||
result = case callback
|
||||
when Symbol
|
||||
self.send(callback)
|
||||
when String
|
||||
@@ -321,9 +328,11 @@ module ActiveRecord
|
||||
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
|
||||
end
|
||||
end
|
||||
return false if result == false
|
||||
end
|
||||
|
||||
invoke_and_notify(method)
|
||||
true
|
||||
end
|
||||
|
||||
def callbacks_for(method)
|
||||
@@ -340,4 +349,4 @@ module ActiveRecord
|
||||
self.class.notify_observers(method, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ module ActiveRecord
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_remove_association_relation(association_name)# :nodoc:
|
||||
def deprecated_remove_association_relation(association_name)# :nodoc:
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def remove_#{association_name}(*items)
|
||||
#{association_name}.delete(items)
|
||||
@@ -50,7 +50,7 @@ module ActiveRecord
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_create_method(collection_name)# :nodoc:
|
||||
def deprecated_collection_create_method(collection_name)# :nodoc:
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def create_in_#{collection_name}(attributes = {})
|
||||
#{collection_name}.create(attributes)
|
||||
@@ -58,13 +58,51 @@ module ActiveRecord
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_build_method(collection_name)# :nodoc:
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def build_to_#{collection_name}(attributes = {})
|
||||
#{collection_name}.build(attributes)
|
||||
def deprecated_collection_build_method(collection_name)# :nodoc:
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def build_to_#{collection_name}(attributes = {})
|
||||
#{collection_name}.build(attributes)
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_association_comparison_method(association_name, association_class_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{association_name}?(comparison_object, force_reload = false)
|
||||
if comparison_object.kind_of?(#{association_class_name})
|
||||
#{association_name}(force_reload) == comparison_object
|
||||
else
|
||||
raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_has_association_method(association_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def has_#{association_name}?(force_reload = false)
|
||||
!#{association_name}(force_reload).nil?
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{method_prefix + collection_name}(attributes = {})
|
||||
association = #{collection_class_name}.new
|
||||
association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
|
||||
association
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
def deprecated_create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def #{method_prefix + collection_name}(attributes = nil)
|
||||
#{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,14 +24,14 @@ class AssociationsTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_force_reload
|
||||
firm = Firm.new
|
||||
firm = Firm.new("name" => "A New Firm, Inc")
|
||||
firm.save
|
||||
firm.clients.each {|c|} # forcing to load all clients
|
||||
assert firm.clients.empty?, "New firm shouldn't have client objects"
|
||||
assert !firm.has_clients?, "New firm shouldn't have clients"
|
||||
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
|
||||
|
||||
client = Client.new("firm_id" => firm.id)
|
||||
client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
|
||||
client.save
|
||||
|
||||
assert firm.clients.empty?, "New firm should have cached no client objects"
|
||||
@@ -72,12 +72,6 @@ class HasOneAssociationsTest < Test::Unit::TestCase
|
||||
def test_has_one
|
||||
assert_equal @signals37.account, Account.find(1)
|
||||
assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
|
||||
assert @signals37.has_account?, "37signals should have an account"
|
||||
assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
|
||||
assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
|
||||
|
||||
assert !Account.find(2).has_firm?, "Unknown isn't linked"
|
||||
assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
|
||||
end
|
||||
|
||||
def test_type_mismatch
|
||||
@@ -100,30 +94,6 @@ class HasOneAssociationsTest < Test::Unit::TestCase
|
||||
assert_nil Account.find(old_account_id).firm_id
|
||||
end
|
||||
|
||||
def test_build
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
|
||||
account = firm.build_account("credit_limit" => 1000)
|
||||
assert account.save
|
||||
assert_equal account, firm.account
|
||||
end
|
||||
|
||||
def test_failing_build_association
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
|
||||
account = firm.build_account
|
||||
assert !account.save
|
||||
assert_equal "can't be empty", account.errors.on("credit_limit")
|
||||
end
|
||||
|
||||
def test_create
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
assert_equal firm.create_account("credit_limit" => 1000), firm.account
|
||||
end
|
||||
|
||||
def test_dependence
|
||||
firm = Firm.find(1)
|
||||
assert !firm.account.nil?
|
||||
@@ -131,12 +101,98 @@ class HasOneAssociationsTest < Test::Unit::TestCase
|
||||
assert_equal 1, Account.find_all.length
|
||||
end
|
||||
|
||||
def test_build
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
|
||||
account = firm.account.build("credit_limit" => 1000)
|
||||
assert_equal account, firm.account
|
||||
assert account.save
|
||||
assert_equal account, firm.account
|
||||
end
|
||||
|
||||
def test_build_before_child_saved
|
||||
firm = Firm.find(1)
|
||||
|
||||
account = firm.account.build("credit_limit" => 1000)
|
||||
assert_equal account, firm.account
|
||||
assert account.new_record?
|
||||
assert firm.save
|
||||
assert_equal account, firm.account
|
||||
assert !account.new_record?
|
||||
end
|
||||
|
||||
def test_build_before_either_saved
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
|
||||
account = firm.account.build("credit_limit" => 1000)
|
||||
assert_equal account, firm.account
|
||||
assert account.new_record?
|
||||
assert firm.save
|
||||
assert_equal account, firm.account
|
||||
assert !account.new_record?
|
||||
end
|
||||
|
||||
def test_failing_build_association
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
|
||||
account = firm.account.build
|
||||
assert_equal account, firm.account
|
||||
assert !account.save
|
||||
assert_equal account, firm.account
|
||||
assert_equal "can't be empty", account.errors.on("credit_limit")
|
||||
end
|
||||
|
||||
def test_create
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
assert_equal firm.account.create("credit_limit" => 1000), firm.account
|
||||
end
|
||||
|
||||
def test_create_before_save
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
assert_equal firm.account.create("credit_limit" => 1000), firm.account
|
||||
end
|
||||
|
||||
def test_dependence_with_missing_association
|
||||
Account.destroy_all
|
||||
firm = Firm.find(1)
|
||||
assert !firm.has_account?
|
||||
firm = Firm.find(1)
|
||||
assert firm.account.nil?
|
||||
firm.destroy
|
||||
end
|
||||
|
||||
def test_assignment_before_parent_saved
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.account = a = Account.find(1)
|
||||
assert firm.new_record?
|
||||
assert_equal a, firm.account
|
||||
assert firm.save
|
||||
assert_equal a, firm.account
|
||||
assert_equal a, firm.account(true)
|
||||
end
|
||||
|
||||
def test_assignment_before_child_saved
|
||||
firm = Firm.find(1)
|
||||
firm.account = a = Account.new("credit_limit" => 1000)
|
||||
assert !a.new_record?
|
||||
assert_equal a, firm.account
|
||||
assert_equal a, firm.account
|
||||
assert_equal a, firm.account(true)
|
||||
end
|
||||
|
||||
def test_assignment_before_either_saved
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.account = a = Account.new("credit_limit" => 1000)
|
||||
assert firm.new_record?
|
||||
assert a.new_record?
|
||||
assert_equal a, firm.account
|
||||
assert firm.save
|
||||
assert !firm.new_record?
|
||||
assert !a.new_record?
|
||||
assert_equal a, firm.account
|
||||
assert_equal a, firm.account(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -257,16 +313,72 @@ class HasManyAssociationsTest < Test::Unit::TestCase
|
||||
assert_equal 3, @signals37.clients_of_firm(true).size
|
||||
end
|
||||
|
||||
def test_adding_before_save
|
||||
no_of_firms = Firm.count
|
||||
no_of_clients = Client.count
|
||||
new_firm = Firm.new("name" => "A New Firm, Inc")
|
||||
new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
|
||||
new_firm.clients_of_firm << (c = Client.new("name" => "Apple"))
|
||||
assert new_firm.new_record?
|
||||
assert c.new_record?
|
||||
assert_equal 2, new_firm.clients_of_firm.size
|
||||
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
|
||||
assert_equal no_of_clients, Client.count # Clients were not saved to database.
|
||||
assert new_firm.save
|
||||
assert !new_firm.new_record?
|
||||
assert !c.new_record?
|
||||
assert_equal new_firm, c.firm
|
||||
assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
|
||||
assert_equal no_of_clients+2, Client.count # Clients were saved to database.
|
||||
assert_equal 2, new_firm.clients_of_firm.size
|
||||
assert_equal 2, new_firm.clients_of_firm(true).size
|
||||
end
|
||||
|
||||
def test_invalid_adding
|
||||
firm = Firm.find(1)
|
||||
assert !(firm.clients_of_firm << c = Client.new)
|
||||
assert c.new_record?
|
||||
assert !firm.save
|
||||
assert c.new_record?
|
||||
end
|
||||
|
||||
def test_invalid_adding_before_save
|
||||
no_of_firms = Firm.count
|
||||
no_of_clients = Client.count
|
||||
new_firm = Firm.new("name" => "A New Firm, Inc")
|
||||
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
|
||||
assert c.new_record?
|
||||
assert !c.valid?
|
||||
assert new_firm.valid?
|
||||
assert !new_firm.save
|
||||
assert c.new_record?
|
||||
assert new_firm.new_record?
|
||||
end
|
||||
|
||||
def test_build
|
||||
new_client = @signals37.clients_of_firm.build("name" => "Another Client")
|
||||
assert_equal "Another Client", new_client.name
|
||||
assert new_client.save
|
||||
assert new_client.new_record?
|
||||
assert_equal new_client, @signals37.clients_of_firm.last
|
||||
assert @signals37.save
|
||||
assert !new_client.new_record?
|
||||
assert_equal 2, @signals37.clients_of_firm(true).size
|
||||
end
|
||||
|
||||
def test_invalid_build
|
||||
new_client = @signals37.clients_of_firm.build
|
||||
assert new_client.new_record?
|
||||
assert !new_client.valid?
|
||||
assert_equal new_client, @signals37.clients_of_firm.last
|
||||
assert !@signals37.save
|
||||
assert new_client.new_record?
|
||||
assert_equal 1, @signals37.clients_of_firm(true).size
|
||||
end
|
||||
|
||||
def test_create
|
||||
force_signal37_to_load_all_clients_of_firm
|
||||
new_client = @signals37.clients_of_firm.create("name" => "Another Client")
|
||||
assert !new_client.new_record?
|
||||
assert_equal new_client, @signals37.clients_of_firm.last
|
||||
assert_equal new_client, @signals37.clients_of_firm(true).last
|
||||
end
|
||||
@@ -278,6 +390,14 @@ class HasManyAssociationsTest < Test::Unit::TestCase
|
||||
assert_equal 0, @signals37.clients_of_firm(true).size
|
||||
end
|
||||
|
||||
def test_deleting_before_save
|
||||
new_firm = Firm.new("name" => "A New Firm, Inc.")
|
||||
new_client = new_firm.clients_of_firm.build("name" => "Another Client")
|
||||
assert_equal 1, new_firm.clients_of_firm.size
|
||||
new_firm.clients_of_firm.delete(new_client)
|
||||
assert_equal 0, new_firm.clients_of_firm.size
|
||||
end
|
||||
|
||||
def test_deleting_a_collection
|
||||
force_signal37_to_load_all_clients_of_firm
|
||||
@signals37.clients_of_firm.create("name" => "Another Client")
|
||||
@@ -419,6 +539,44 @@ class BelongsToAssociationsTest < Test::Unit::TestCase
|
||||
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
|
||||
end
|
||||
|
||||
def test_assignment_before_parent_saved
|
||||
client = Client.find_first
|
||||
apple = Firm.new("name" => "Apple")
|
||||
client.firm = apple
|
||||
assert_equal apple, client.firm
|
||||
assert apple.new_record?
|
||||
assert client.save
|
||||
assert apple.save
|
||||
assert !apple.new_record?
|
||||
assert_equal apple, client.firm
|
||||
assert_equal apple, client.firm(true)
|
||||
end
|
||||
|
||||
def test_assignment_before_child_saved
|
||||
final_cut = Client.new("name" => "Final Cut")
|
||||
firm = Firm.find(1)
|
||||
final_cut.firm = firm
|
||||
assert final_cut.new_record?
|
||||
assert final_cut.save
|
||||
assert !final_cut.new_record?
|
||||
assert !firm.new_record?
|
||||
assert_equal firm, final_cut.firm
|
||||
assert_equal firm, final_cut.firm(true)
|
||||
end
|
||||
|
||||
def test_assignment_before_either_saved
|
||||
final_cut = Client.new("name" => "Final Cut")
|
||||
apple = Firm.new("name" => "Apple")
|
||||
final_cut.firm = apple
|
||||
assert final_cut.new_record?
|
||||
assert apple.new_record?
|
||||
assert final_cut.save
|
||||
assert !final_cut.new_record?
|
||||
assert !apple.new_record?
|
||||
assert_equal apple, final_cut.firm
|
||||
assert_equal apple, final_cut.firm(true)
|
||||
end
|
||||
|
||||
def test_field_name_same_as_foreign_key
|
||||
computer = Computer.find 1
|
||||
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up"
|
||||
@@ -514,6 +672,38 @@ class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase
|
||||
assert_equal 2, aridridel.projects.size
|
||||
assert_equal 2, aridridel.projects(true).size
|
||||
end
|
||||
|
||||
def test_habtm_adding_before_save
|
||||
no_of_devels = Developer.count
|
||||
no_of_projects = Project.count
|
||||
aridridel = Developer.new("name" => "Aridridel")
|
||||
aridridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
|
||||
assert aridridel.new_record?
|
||||
assert p.new_record?
|
||||
assert aridridel.save
|
||||
assert !aridridel.new_record?
|
||||
assert_equal no_of_devels+1, Developer.count
|
||||
assert_equal no_of_projects+1, Project.count
|
||||
assert_equal 2, aridridel.projects.size
|
||||
assert_equal 2, aridridel.projects(true).size
|
||||
end
|
||||
|
||||
def test_build
|
||||
devel = Developer.find(1)
|
||||
proj = devel.projects.build("name" => "Projekt")
|
||||
assert_equal devel.projects.last, proj
|
||||
assert proj.new_record?
|
||||
devel.save
|
||||
assert !proj.new_record?
|
||||
assert_equal devel.projects.last, proj
|
||||
end
|
||||
|
||||
def test_create
|
||||
devel = Developer.find(1)
|
||||
proj = devel.projects.create("name" => "Projekt")
|
||||
assert_equal devel.projects.last, proj
|
||||
assert !proj.new_record?
|
||||
end
|
||||
|
||||
def test_uniq_after_the_fact
|
||||
@developers["jamis"].find.projects << @projects["active_record"].find
|
||||
|
||||
@@ -227,4 +227,20 @@ class CallbacksTest < Test::Unit::TestCase
|
||||
[ :after_initialize, :method ]
|
||||
], david.history
|
||||
end
|
||||
|
||||
def test_zzz_callback_returning_false # must be run last since we modify CallbackDeveloper
|
||||
david = CallbackDeveloper.find(1)
|
||||
CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false }
|
||||
CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
|
||||
david.save
|
||||
assert_equal [
|
||||
[ :after_find, :method ],
|
||||
[ :after_initialize, :method ],
|
||||
[ :before_validation, :string ],
|
||||
[ :before_validation, :proc ],
|
||||
[ :before_validation, :object ],
|
||||
[ :before_validation, :block ],
|
||||
[ :before_validation, :returning_false ]
|
||||
], david.history
|
||||
end
|
||||
end
|
||||
|
||||
@@ -138,14 +138,14 @@ class DeprecatedAssociationsTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_force_reload
|
||||
firm = Firm.new
|
||||
firm = Firm.new("name" => "A New Firm, Inc")
|
||||
firm.save
|
||||
firm.clients.each {|c|} # forcing to load all clients
|
||||
assert firm.clients.empty?, "New firm shouldn't have client objects"
|
||||
assert !firm.has_clients?, "New firm shouldn't have clients"
|
||||
assert_equal 0, firm.clients_count, "New firm should have 0 clients"
|
||||
|
||||
client = Client.new("firm_id" => firm.id)
|
||||
client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
|
||||
client.save
|
||||
|
||||
assert firm.clients.empty?, "New firm should have cached no client objects"
|
||||
@@ -340,4 +340,38 @@ class DeprecatedAssociationsTest < Test::Unit::TestCase
|
||||
assert_equal 2, Firm.find_first.find_all_in_clients("type = 'Client'").length
|
||||
assert_equal 1, Firm.find_first.find_all_in_clients("name = 'Summit'").length
|
||||
end
|
||||
|
||||
def test_has_one
|
||||
assert @signals37.account?(Account.find(1))
|
||||
assert @signals37.has_account?, "37signals should have an account"
|
||||
assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
|
||||
assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
|
||||
|
||||
assert !Account.find(2).has_firm?, "Unknown isn't linked"
|
||||
assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
|
||||
end
|
||||
|
||||
def test_has_one_build
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
assert firm.save
|
||||
|
||||
account = firm.build_account("credit_limit" => 1000)
|
||||
assert account.save
|
||||
assert_equal account, firm.account
|
||||
end
|
||||
|
||||
def test_has_one_failing_build_association
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
|
||||
account = firm.build_account
|
||||
assert !account.save
|
||||
assert_equal "can't be empty", account.errors.on("credit_limit")
|
||||
end
|
||||
|
||||
def test_has_one_create
|
||||
firm = Firm.new("name" => "GlobalMegaCorp")
|
||||
firm.save
|
||||
assert_equal firm.create_account("credit_limit" => 1000), firm.account
|
||||
end
|
||||
end
|
||||
|
||||
2
activerecord/test/fixtures/company.rb
vendored
2
activerecord/test/fixtures/company.rb
vendored
@@ -1,5 +1,7 @@
|
||||
class Company < ActiveRecord::Base
|
||||
attr_protected :rating
|
||||
|
||||
validates_presence_of :name
|
||||
end
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user