Permalink
Browse files

some progress on has_one association testing. ran into wall trying to…

… replace an existing singular association.
  • Loading branch information...
1 parent d06a4b0 commit 69908c4e0a12c3da4c45a8a27db32dc392b3e534 @cainlevy committed Dec 12, 2008
Showing with 98 additions and 14 deletions.
  1. +25 −9 lib/nested_assignment.rb
  2. +13 −1 test/db/models.rb
  3. +4 −3 test/test_helper.rb
  4. +56 −1 test/unit/nested_assignment_test.rb
View
@@ -1,7 +1,13 @@
# NestedAssignment
module NestedAssignment
def self.included(base)
- base.class_eval { extend ClassMethods }
+ base.class_eval do
+ extend ClassMethods
+
+ alias_method_chain :save, :associated
+ alias_method_chain :valid?, :associated
+ alias_method_chain :changed?, :associated
+ end
end
module ClassMethods
@@ -12,7 +18,13 @@ def accessible_associations(*associations)
define_method("#{name}_params=") do |hash|
assoc = self.send(name)
hash.values.each do |row|
- record = row[:id].blank? ? assoc.build : assoc.select{|r| r.id == row[:id].to_i}
+ # TODO: need to bypass the replace() call inside singular associations (has_one and belongs_to). but they
+ # do serve a purpose: disassociating or destroying an existing record. if that is not to happen during
+ # assignment, then those records need to be collected for later disassociation (or removal, if :dependent
+ # => :destroy). that would need to be part of the saving process. ALSO, this makes sense to handle while
+ # deleting from plural associations. so perhaps instead of setting #_delete, i should add to a
+ # disassociation hash for later.
+ record = row[:id].blank? ? assoc.build : [assoc].flatten.detect{|r| r.id == row[:id].to_i}
if row[:_delete]
record._delete = true
else
@@ -29,26 +41,30 @@ def association_names
end
end
- # marks the record to be deleted in the next save
+ # marks the (associated) record to be deleted in the next deep save
attr_accessor :_delete
# deep validation of any changed (or new) records.
# makes sure that any single invalid record will not halt the
# validation process, so that all errors will be available
# afterwards.
- def valid?
- [changed_associated.all?(&:valid?), super].all?
+ def valid_with_associated?
+ [changed_associated.all?(&:valid?), valid_without_associated?].all?
end
# deep saving of any new, changed, or deleted records.
- def save
+ def save_with_associated
self.class.transaction do
- super
- changed_associated.each(&:save)
- deletable_associated.each(&:destroy)
+ changed_associated.all?(&:save) &&
+ deletable_associated.all?(&:destroy) &&
+ save_without_associated
end
end
+ def changed_with_associated?
+ changed_without_associated? or instantiated_associated.any?(&:changed?)
+ end
+
protected
def deletable_associated
View
@@ -14,25 +14,37 @@ def self.included(base)
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
+
+ validates_presence_of :name
end
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
has_one :subscription
has_one :address, :as => :addressable
+
+ accessible_associations :subscription
+
+ validates_presence_of :name
end
class Service < ActiveRecord::Base
has_many :subscriptions
has_many :users, :through => :subscriptions
+
+ validates_presence_of :name
end
class Subscription < ActiveRecord::Base
belongs_to :service
belongs_to :user
+
+ validates_presence_of :name
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
+
+ validates_presence_of :name
end
-end
+end
View
@@ -6,6 +6,10 @@
require 'active_record'
require 'active_record/fixtures'
+# load the code-to-be-tested
+ActiveSupport::Dependencies.load_paths << File.dirname(__FILE__) + '/../lib/'
+require File.dirname(__FILE__) + '/../init'
+
# establish the database connection
ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/db/database.yml'))
ActiveRecord::Base.establish_connection('nested_assignment_test')
@@ -31,6 +35,3 @@ class ActiveSupport::TestCase
fixtures :all
end
-# load the code-to-be-tested
-ActiveSupport::Dependencies.load_paths << File.dirname(__FILE__) + '/../lib/'
-require File.dirname(__FILE__) + '/../init'
@@ -2,8 +2,63 @@
require 'ruby-debug'
-class NestedAssignmentTest < ActiveSupport::TestCase
+class NestedAssignmentHasOneTest < ActiveSupport::TestCase
+ def setup
+ @user = users(:bob)
+ @subscription = subscriptions(:bob_is_free)
+ end
+
+ def test_updating_a_subscription
+ @user.subscription_params = {
+ "1" => {
+ :id => @subscription.id,
+ :name => "Bobtastic"
+ }
+ }
+ assert !@user.subscription.new_record?, "the association was not rebuilt"
+ assert_equal "Bobtastic", @user.subscription.name, "the existing subscription's name has changed"
+ assert_equal "Bob/Free", @subscription.reload.name, "the name change has not been saved"
+ end
+
+ def test_assigning_a_replacement_subscription
+ @user.subscription_params = {
+ "1" => {
+ :name => "Bobtastic"
+ }
+ }
+ assert @user.subscription.new_record?, "the association is a new object"
+ assert_equal "Bobtastic", @user.subscription.name, "the new record has the specified name"
+ assert !@subscription.reload.user_id.nil?, "the previously associated object has not been disassociated yet"
+ end
+
+ def test_assigning_a_removed_subscription
+ @user.subscription_params = {
+ "1" => {
+ :id => @subscription.id,
+ :name => "Bobtastic",
+ :_delete => "1"
+ }
+ }
+ assert @user.subscription._delete, "the association is marked for deletion"
+ assert_nothing_raised("the associated object has not been deleted yet") do @subscription.reload end
+ assert_equal "Bob/Free", @user.subscription.name, "the association attribute did not update"
+ end
+
+end
+
+class NestedAssignmentBelongsToTest < ActiveSupport::TestCase
+end
+
+class NestedAssignmentHasManyTest < ActiveSupport::TestCase
+end
+
+class NestedAssignmentHasAndBelongsToManyTest < ActiveSupport::TestCase
+end
+
+class NestedAssignmentHasManyThroughTest < ActiveSupport::TestCase
+end
+class NestedAssignmentHelperTest < ActiveSupport::TestCase
def test_association_names
assert_equal [:address, :roles, :subscription], User.association_names.sort_by(&:to_s)
end

0 comments on commit 69908c4

Please sign in to comment.