Add :validate option to associations. [#301 state:resolved]

Signed-off-by: Pratik Naik <pratiknaik@gmail.com>
This commit is contained in:
Jan De Poorter
2008-06-11 13:08:35 +02:00
committed by Pratik Naik
parent f728e57d22
commit 7f140bbdda
8 changed files with 67 additions and 12 deletions

View File

@@ -1,5 +1,7 @@
*Edge*
* Add :validate option to associations to enable/disable the automatic validation of associated models. Resolves #301. [Jan De Poorter]
* PostgreSQL: use 'INSERT ... RETURNING id' for 8.2 and later. [Jeremy Kemper]
* Added SQL escaping for :limit and :offset in MySQL [Jonathan Wiess]

View File

@@ -690,6 +690,7 @@ module ActiveRecord
# association is a polymorphic +belongs_to+.
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
#
# Option examples:
# has_many :comments, :order => "posted_on"
@@ -710,7 +711,7 @@ module ActiveRecord
configure_dependency_for_has_many(reflection)
add_multiple_associated_save_callbacks(reflection.name)
add_multiple_associated_save_callbacks(reflection.name) unless options[:validate] == false
add_association_callbacks(reflection.name, reflection.options)
if options[:through]
@@ -769,6 +770,7 @@ module ActiveRecord
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@@ -799,7 +801,7 @@ module ActiveRecord
end
after_save method_name
add_single_associated_save_callbacks(reflection.name)
add_single_associated_save_callbacks(reflection.name) if options[:validate] == true
association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation)
@@ -857,6 +859,7 @@ module ActiveRecord
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -937,6 +940,8 @@ module ActiveRecord
)
end
add_single_associated_save_callbacks(reflection.name) unless options[:validate] == false
configure_dependency_for_belongs_to(reflection)
end
@@ -1025,6 +1030,7 @@ module ActiveRecord
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
#
# Option examples:
# has_and_belongs_to_many :projects
@@ -1037,7 +1043,7 @@ module ActiveRecord
def has_and_belongs_to_many(association_id, options = {}, &extension)
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
add_multiple_associated_save_callbacks(reflection.name)
add_multiple_associated_save_callbacks(reflection.name) unless options[:validate] == false
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
# Don't use a before_destroy callback since users' before_destroy
@@ -1343,7 +1349,8 @@ module ActiveRecord
:uniq,
:finder_sql, :counter_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend, :readonly
:extend, :readonly,
:validate
)
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
@@ -1353,7 +1360,7 @@ module ActiveRecord
def create_has_one_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate
)
create_reflection(:has_one, association_id, options, self)
@@ -1361,7 +1368,7 @@ module ActiveRecord
def create_has_one_through_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
)
create_reflection(:has_one, association_id, options, self)
end
@@ -1369,7 +1376,7 @@ module ActiveRecord
def create_belongs_to_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
:counter_cache, :extend, :polymorphic, :readonly
:counter_cache, :extend, :polymorphic, :readonly, :validate
)
reflection = create_reflection(:belongs_to, association_id, options, self)
@@ -1388,7 +1395,8 @@ module ActiveRecord
:uniq,
:finder_sql, :delete_sql, :insert_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend, :readonly
:extend, :readonly,
:validate
)
options[:extend] = create_extension_modules(association_id, extension, options[:extend])

View File

@@ -409,4 +409,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable = new_member
assert_equal nil, sponsor.sponsorable_id
end
def test_save_fails_for_invalid_belongs_to
assert log = AuditLog.create(:developer_id=>0,:message=>"")
log.developer = Developer.new
assert !log.developer.valid?
assert !log.valid?
assert !log.save
assert_equal "is invalid", log.errors.on("developer")
end
def test_save_succeeds_for_invalid_belongs_to_with_validate_false
assert log = AuditLog.create(:developer_id=>0,:message=>"")
log.unvalidated_developer = Developer.new
assert !log.unvalidated_developer.valid?
assert log.valid?
assert log.save
end
end

View File

@@ -342,6 +342,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert new_firm.new_record?
end
def test_invalid_adding_with_validate_false
firm = Firm.find(:first)
client = Client.new
firm.unvalidated_clients_of_firm << Client.new
assert firm.valid?
assert !client.valid?
assert firm.save
assert client.new_record?
end
def test_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }

View File

@@ -275,6 +275,18 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal "is invalid", firm.errors.on("account")
end
def test_save_succeeds_for_invalid_has_one_with_validate_false
firm = Firm.find(:first)
assert firm.valid?
firm.unvalidated_account = Account.new
assert !firm.unvalidated_account.valid?
assert firm.valid?
assert firm.save
end
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)

View File

@@ -160,9 +160,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
assert_equal 20, Firm.reflect_on_all_associations.size
assert_equal 16, Firm.reflect_on_all_associations(:has_many).size
assert_equal 4, Firm.reflect_on_all_associations(:has_one).size
assert_equal 22, Firm.reflect_on_all_associations.size
assert_equal 17, Firm.reflect_on_all_associations(:has_many).size
assert_equal 5, Firm.reflect_on_all_associations(:has_one).size
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end

View File

@@ -26,6 +26,7 @@ class Firm < Company
"AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )"
has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
has_many :dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :destroy
has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all
has_many :limited_clients, :class_name => "Client", :order => "id", :limit => 1
@@ -46,7 +47,8 @@ class Firm < Company
has_many :plain_clients, :class_name => 'Client'
has_many :readonly_clients, :class_name => 'Client', :readonly => true
has_one :account, :foreign_key => "firm_id", :dependent => :destroy
has_one :account, :foreign_key => "firm_id", :dependent => :destroy, :validate => true
has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false
has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account'
has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true
end

View File

@@ -57,6 +57,7 @@ end
class AuditLog < ActiveRecord::Base
belongs_to :developer
belongs_to :unvalidated_developer, :class_name => 'Developer', :validate => false
end
DeveloperSalary = Struct.new(:amount)