Skip to content

Commit

Permalink
Merge remote branch 'eloy/2-3-stable' into 2-3-stable
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Jan 8, 2010
2 parents 94de32b + 51e6124 commit c50609c
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 31 deletions.
6 changes: 3 additions & 3 deletions activerecord/lib/active_record/associations.rb
Expand Up @@ -1783,7 +1783,7 @@ def references_eager_loaded_tables?(options)
end end


def using_limitable_reflections?(reflections) def using_limitable_reflections?(reflections)
reflections.collect(&:collection_association?).length.zero? reflections.collect(&:collection?).length.zero?
end end


def column_aliases(join_dependency) def column_aliases(join_dependency)
Expand Down Expand Up @@ -1860,7 +1860,7 @@ def remove_duplicate_results!(base, records, associations)
case associations case associations
when Symbol, String when Symbol, String
reflection = base.reflections[associations] reflection = base.reflections[associations]
if reflection && reflection.collection_association? if reflection && reflection.collection?
records.each { |record| record.send(reflection.name).target.uniq! } records.each { |record| record.send(reflection.name).target.uniq! }
end end
when Array when Array
Expand All @@ -1874,7 +1874,7 @@ def remove_duplicate_results!(base, records, associations)
parent_records = records.map do |record| parent_records = records.map do |record|
descendant = record.send(reflection.name) descendant = record.send(reflection.name)
next unless descendant next unless descendant
descendant.target.uniq! if reflection.collection_association? descendant.target.uniq! if reflection.collection?
descendant descendant
end.flatten.compact end.flatten.compact


Expand Down
22 changes: 14 additions & 8 deletions activerecord/lib/active_record/autosave_association.rb
Expand Up @@ -167,7 +167,7 @@ def #{type}(name, options = {})
def add_autosave_association_callbacks(reflection) def add_autosave_association_callbacks(reflection)
save_method = :"autosave_associated_records_for_#{reflection.name}" save_method = :"autosave_associated_records_for_#{reflection.name}"
validation_method = :"validate_associated_records_for_#{reflection.name}" validation_method = :"validate_associated_records_for_#{reflection.name}"
collection = reflection.collection_association? collection = reflection.collection?


unless method_defined?(save_method) unless method_defined?(save_method)
if collection if collection
Expand Down Expand Up @@ -225,10 +225,10 @@ def marked_for_destruction?
def associated_records_to_validate_or_save(association, new_record, autosave) def associated_records_to_validate_or_save(association, new_record, autosave)
if new_record if new_record
association association
elsif association.loaded? elsif autosave
autosave ? association : association.select { |record| record.new_record? } association.target.select { |record| record.new_record? || record.changed? || record.marked_for_destruction? }
else else
autosave ? association.target : association.target.select { |record| record.new_record? } association.target.select { |record| record.new_record? }
end end
end end


Expand Down Expand Up @@ -297,13 +297,15 @@ def save_collection_association(reflection)
association.destroy(record) association.destroy(record)
elsif autosave != false && (@new_record_before_save || record.new_record?) elsif autosave != false && (@new_record_before_save || record.new_record?)
if autosave if autosave
association.send(:insert_record, record, false, false) saved = association.send(:insert_record, record, false, false)
else else
association.send(:insert_record, record) association.send(:insert_record, record)
end end
elsif autosave elsif autosave
record.save(false) saved = record.save(false)
end end

raise ActiveRecord::Rollback if saved == false
end end
end end


Expand All @@ -330,7 +332,9 @@ def save_has_one_association(reflection)
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
association[reflection.primary_key_name] = key association[reflection.primary_key_name] = key
association.save(!autosave) saved = association.save(!autosave)
raise ActiveRecord::Rollback if !saved && autosave
saved
end end
end end
end end
Expand All @@ -351,7 +355,7 @@ def save_belongs_to_association(reflection)
if autosave && association.marked_for_destruction? if autosave && association.marked_for_destruction?
association.destroy association.destroy
elsif autosave != false elsif autosave != false
association.save(!autosave) if association.new_record? || autosave saved = association.save(!autosave) if association.new_record? || autosave


if association.updated? if association.updated?
association_id = association.send(reflection.options[:primary_key] || :id) association_id = association.send(reflection.options[:primary_key] || :id)
Expand All @@ -361,6 +365,8 @@ def save_belongs_to_association(reflection)
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
end end
end end

saved if autosave
end end
end end
end end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/nested_attributes.rb
Expand Up @@ -234,7 +234,7 @@ def accepts_nested_attributes_for(*attr_names)
reflection.options[:autosave] = true reflection.options[:autosave] = true
add_autosave_association_callbacks(reflection) add_autosave_association_callbacks(reflection)
nested_attributes_options[association_name.to_sym] = options nested_attributes_options[association_name.to_sym] = options
type = (reflection.collection_association? ? :collection : :one_to_one) type = (reflection.collection? ? :collection : :one_to_one)


# def pirate_attributes=(attributes) # def pirate_attributes=(attributes)
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
Expand Down
10 changes: 5 additions & 5 deletions activerecord/lib/active_record/reflection.rb
Expand Up @@ -257,11 +257,11 @@ def polymorphic_inverse_of(associated_class)
# Returns whether or not this association reflection is for a collection # Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is one of +has_many+ or # association. Returns +true+ if the +macro+ is one of +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise. # +has_and_belongs_to_many+, +false+ otherwise.
def collection_association? def collection?
if @collection_association.nil? if @collection.nil?
@collection_association = [:has_many, :has_and_belongs_to_many].include?(macro) @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
end end
@collection_association @collection
end end


# Returns whether or not the association should be validated as part of # Returns whether or not the association should be validated as part of
Expand All @@ -280,7 +280,7 @@ def validate?
private private
def derive_class_name def derive_class_name
class_name = name.to_s.camelize class_name = name.to_s.camelize
class_name = class_name.singularize if collection_association? class_name = class_name.singularize if collection?
class_name class_name
end end


Expand Down
67 changes: 57 additions & 10 deletions activerecord/test/cases/autosave_association_test.rb
Expand Up @@ -3,6 +3,8 @@
require 'models/company' require 'models/company'
require 'models/customer' require 'models/customer'
require 'models/developer' require 'models/developer'
require 'models/invoice'
require 'models/line_item'
require 'models/order' require 'models/order'
require 'models/parrot' require 'models/parrot'
require 'models/person' require 'models/person'
Expand Down Expand Up @@ -692,23 +694,18 @@ def save(*args)


define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do
2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") } 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
before = @pirate.send(association_name).map { |c| c } before = @pirate.send(association_name).map { |c| c.mark_for_destruction ; c }


# Stub the save method of the first child to destroy and the second to raise an exception # Stub the destroy method of the the second child to raise an exception
class << before.first
def save(*args)
super
destroy
end
end
class << before.last class << before.last
def save(*args) def destroy(*args)
super super
raise 'Oh noes!' raise 'Oh noes!'
end end
end end


assert_raise(RuntimeError) { assert !@pirate.save } assert_raise(RuntimeError) { assert !@pirate.save }
assert before.first.frozen? # the first child was indeed destroyed
assert_equal before, @pirate.reload.send(association_name) assert_equal before, @pirate.reload.send(association_name)
end end


Expand Down Expand Up @@ -816,6 +813,18 @@ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_t
end end
end end


def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
pirate = Pirate.new(:catchphrase => 'Arr')
ship = pirate.build_ship(:name => 'The Vile Insanity')
ship.cancel_save_from_callback = true

assert_no_difference 'Pirate.count' do
assert_no_difference 'Ship.count' do
assert !pirate.save
end
end
end

def test_should_rollback_any_changes_if_an_exception_occurred_while_saving def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, @pirate.ship.name] before = [@pirate.catchphrase, @pirate.ship.name]


Expand Down Expand Up @@ -894,6 +903,18 @@ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_t
end end
end end


def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
ship = Ship.new(:name => 'The Vile Insanity')
pirate = ship.build_pirate(:catchphrase => 'Arr')
pirate.cancel_save_from_callback = true

assert_no_difference 'Ship.count' do
assert_no_difference 'Pirate.count' do
assert !ship.save
end
end
end

def test_should_rollback_any_changes_if_an_exception_occurred_while_saving def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@ship.pirate.catchphrase, @ship.name] before = [@ship.pirate.catchphrase, @ship.name]


Expand All @@ -909,7 +930,6 @@ def save(*args)
end end


assert_raise(RuntimeError) { assert !@ship.save } assert_raise(RuntimeError) { assert !@ship.save }
# TODO: Why does using reload on @ship looses the associated pirate?
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name] assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
end end


Expand Down Expand Up @@ -986,6 +1006,26 @@ def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
end end
end end


def test_should_not_save_and_return_false_if_a_callback_cancelled_saving_in_either_create_or_update
@pirate.catchphrase = 'Changed'
@child_1.name = 'Changed'
@child_1.cancel_save_from_callback = true

assert !@pirate.save
assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase
assert_equal "Posideons Killer", @child_1.reload.name

new_pirate = Pirate.new(:catchphrase => 'Arr')
new_child = new_pirate.send(@association_name).build(:name => 'Grace OMalley')
new_child.cancel_save_from_callback = true

assert_no_difference 'Pirate.count' do
assert_no_difference "#{new_child.class.name}.count" do
assert !new_pirate.save
end
end
end

def test_should_rollback_any_changes_if_an_exception_occurred_while_saving def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)] before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
new_names = ['Grace OMalley', 'Privateers Greed'] new_names = ['Grace OMalley', 'Privateers Greed']
Expand Down Expand Up @@ -1169,3 +1209,10 @@ def setup
assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots) assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots)
end end
end end

class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
def test_autosave_with_touch_should_not_raise_system_stack_error
invoice = Invoice.create
assert_nothing_raised { invoice.line_items.create(:amount => 10) }
end
end
8 changes: 4 additions & 4 deletions activerecord/test/cases/reflection_test.rb
Expand Up @@ -188,11 +188,11 @@ def test_has_many_through_reflection
end end


def test_collection_association def test_collection_association
assert Pirate.reflect_on_association(:birds).collection_association? assert Pirate.reflect_on_association(:birds).collection?
assert Pirate.reflect_on_association(:parrots).collection_association? assert Pirate.reflect_on_association(:parrots).collection?


assert !Pirate.reflect_on_association(:ship).collection_association? assert !Pirate.reflect_on_association(:ship).collection?
assert !Ship.reflect_on_association(:pirate).collection_association? assert !Ship.reflect_on_association(:pirate).collection?
end end


def test_default_association_validation def test_default_association_validation
Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/models/bird.rb
@@ -1,3 +1,9 @@
class Bird < ActiveRecord::Base class Bird < ActiveRecord::Base
validates_presence_of :name validates_presence_of :name

attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
false
end
end end
4 changes: 4 additions & 0 deletions activerecord/test/models/invoice.rb
@@ -0,0 +1,4 @@
class Invoice < ActiveRecord::Base
has_many :line_items, :autosave => true
before_save {|record| record.balance = record.line_items.map(&:amount).sum }
end
3 changes: 3 additions & 0 deletions activerecord/test/models/line_item.rb
@@ -0,0 +1,3 @@
class LineItem < ActiveRecord::Base
belongs_to :invoice, :touch => true
end
6 changes: 6 additions & 0 deletions activerecord/test/models/parrot.rb
Expand Up @@ -6,6 +6,12 @@ class Parrot < ActiveRecord::Base
alias_attribute :title, :name alias_attribute :title, :name


validates_presence_of :name validates_presence_of :name

attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
false
end
end end


class LiveParrot < Parrot class LiveParrot < Parrot
Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/models/pirate.rb
Expand Up @@ -51,6 +51,12 @@ def reject_empty_ships_on_create(attributes)
attributes.delete('_reject_me_if_new').present? && new_record? attributes.delete('_reject_me_if_new').present? && new_record?
end end


attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
false
end

private private
def log_before_add(record) def log_before_add(record)
log(record, "before_adding_method") log(record, "before_adding_method")
Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/models/ship.rb
Expand Up @@ -9,4 +9,10 @@ class Ship < ActiveRecord::Base
accepts_nested_attributes_for :update_only_pirate, :update_only => true accepts_nested_attributes_for :update_only_pirate, :update_only => true


validates_presence_of :name validates_presence_of :name

attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
def cancel_save_callback_method
false
end
end end
10 changes: 10 additions & 0 deletions activerecord/test/schema/schema.rb
Expand Up @@ -192,6 +192,11 @@ def create_table(*args, &block)
t.string :info t.string :info
end end


create_table :invoices, :force => true do |t|
t.integer :balance
t.datetime :updated_at
end

create_table :items, :force => true do |t| create_table :items, :force => true do |t|
t.column :name, :integer t.column :name, :integer
end end
Expand All @@ -217,6 +222,11 @@ def create_table(*args, &block)
t.integer :version, :null => false, :default => 0 t.integer :version, :null => false, :default => 0
end end


create_table :line_items, :force => true do |t|
t.integer :invoice_id
t.integer :amount
end

create_table :lock_without_defaults, :force => true do |t| create_table :lock_without_defaults, :force => true do |t|
t.column :lock_version, :integer t.column :lock_version, :integer
end end
Expand Down

0 comments on commit c50609c

Please sign in to comment.