activerecord 2.2.3 → 2.3.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +438 -396
- data/Rakefile +4 -2
- data/lib/active_record.rb +46 -43
- data/lib/active_record/association_preload.rb +34 -19
- data/lib/active_record/associations.rb +193 -251
- data/lib/active_record/associations/association_collection.rb +38 -21
- data/lib/active_record/associations/association_proxy.rb +11 -4
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
- data/lib/active_record/associations/has_many_association.rb +2 -2
- data/lib/active_record/associations/has_many_through_association.rb +8 -8
- data/lib/active_record/associations/has_one_association.rb +11 -2
- data/lib/active_record/attribute_methods.rb +1 -0
- data/lib/active_record/autosave_association.rb +349 -0
- data/lib/active_record/base.rb +292 -106
- data/lib/active_record/batches.rb +73 -0
- data/lib/active_record/calculations.rb +34 -16
- data/lib/active_record/callbacks.rb +37 -8
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +16 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +103 -15
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -6
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +28 -25
- data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -5
- data/lib/active_record/connection_adapters/mysql_adapter.rb +50 -21
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -41
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +41 -21
- data/lib/active_record/dirty.rb +1 -1
- data/lib/active_record/dynamic_scope_match.rb +25 -0
- data/lib/active_record/fixtures.rb +193 -198
- data/lib/active_record/locale/en.yml +1 -1
- data/lib/active_record/locking/optimistic.rb +33 -0
- data/lib/active_record/migration.rb +8 -2
- data/lib/active_record/named_scope.rb +13 -6
- data/lib/active_record/nested_attributes.rb +329 -0
- data/lib/active_record/query_cache.rb +25 -13
- data/lib/active_record/reflection.rb +6 -1
- data/lib/active_record/schema_dumper.rb +2 -0
- data/lib/active_record/serialization.rb +3 -1
- data/lib/active_record/serializers/json_serializer.rb +19 -0
- data/lib/active_record/serializers/xml_serializer.rb +28 -13
- data/lib/active_record/session_store.rb +318 -0
- data/lib/active_record/test_case.rb +15 -9
- data/lib/active_record/timestamp.rb +2 -2
- data/lib/active_record/transactions.rb +58 -8
- data/lib/active_record/validations.rb +29 -24
- data/lib/active_record/version.rb +2 -2
- data/test/cases/ar_schema_test.rb +0 -1
- data/test/cases/associations/belongs_to_associations_test.rb +35 -131
- data/test/cases/associations/cascaded_eager_loading_test.rb +8 -0
- data/test/cases/associations/eager_load_nested_include_test.rb +29 -0
- data/test/cases/associations/eager_test.rb +137 -7
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +45 -7
- data/test/cases/associations/has_many_associations_test.rb +110 -149
- data/test/cases/associations/has_many_through_associations_test.rb +39 -7
- data/test/cases/associations/has_one_associations_test.rb +39 -92
- data/test/cases/associations/has_one_through_associations_test.rb +34 -3
- data/test/cases/associations/inner_join_association_test.rb +0 -5
- data/test/cases/associations/join_model_test.rb +5 -7
- data/test/cases/attribute_methods_test.rb +13 -1
- data/test/cases/autosave_association_test.rb +901 -0
- data/test/cases/base_test.rb +41 -21
- data/test/cases/batches_test.rb +61 -0
- data/test/cases/calculations_test.rb +37 -17
- data/test/cases/callbacks_test.rb +43 -5
- data/test/cases/connection_pool_test.rb +25 -0
- data/test/cases/copy_table_test_sqlite.rb +11 -0
- data/test/cases/datatype_test_postgresql.rb +1 -0
- data/test/cases/defaults_test.rb +37 -26
- data/test/cases/dirty_test.rb +26 -2
- data/test/cases/finder_test.rb +79 -44
- data/test/cases/fixtures_test.rb +15 -19
- data/test/cases/helper.rb +26 -19
- data/test/cases/inheritance_test.rb +2 -2
- data/test/cases/json_serialization_test.rb +1 -1
- data/test/cases/locking_test.rb +23 -5
- data/test/cases/method_scoping_test.rb +126 -3
- data/test/cases/migration_test.rb +253 -237
- data/test/cases/named_scope_test.rb +73 -3
- data/test/cases/nested_attributes_test.rb +509 -0
- data/test/cases/query_cache_test.rb +0 -4
- data/test/cases/reflection_test.rb +13 -3
- data/test/cases/reload_models_test.rb +3 -1
- data/test/cases/repair_helper.rb +50 -0
- data/test/cases/schema_dumper_test.rb +0 -1
- data/test/cases/transactions_test.rb +177 -12
- data/test/cases/validations_i18n_test.rb +288 -294
- data/test/cases/validations_test.rb +230 -180
- data/test/cases/xml_serialization_test.rb +19 -1
- data/test/fixtures/fixture_database.sqlite3 +0 -0
- data/test/fixtures/fixture_database_2.sqlite3 +0 -0
- data/test/fixtures/member_types.yml +6 -0
- data/test/fixtures/members.yml +3 -1
- data/test/fixtures/people.yml +10 -1
- data/test/fixtures/toys.yml +4 -0
- data/test/models/author.rb +1 -2
- data/test/models/bird.rb +3 -0
- data/test/models/category.rb +1 -0
- data/test/models/company.rb +3 -0
- data/test/models/developer.rb +12 -0
- data/test/models/event.rb +3 -0
- data/test/models/member.rb +1 -0
- data/test/models/member_detail.rb +1 -0
- data/test/models/member_type.rb +3 -0
- data/test/models/owner.rb +2 -1
- data/test/models/parrot.rb +2 -0
- data/test/models/person.rb +6 -0
- data/test/models/pet.rb +2 -1
- data/test/models/pirate.rb +55 -1
- data/test/models/post.rb +6 -0
- data/test/models/project.rb +1 -0
- data/test/models/reply.rb +6 -0
- data/test/models/ship.rb +8 -1
- data/test/models/ship_part.rb +5 -0
- data/test/models/topic.rb +13 -1
- data/test/models/toy.rb +4 -0
- data/test/schema/schema.rb +35 -2
- metadata +70 -9
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -15,7 +15,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|
15
15
|
assert_equal Topic.find(:all), Topic.base
|
16
16
|
assert_equal Topic.find(:all), Topic.base.to_a
|
17
17
|
assert_equal Topic.find(:first), Topic.base.first
|
18
|
-
assert_equal Topic.find(:all), Topic.base.
|
18
|
+
assert_equal Topic.find(:all), Topic.base.map { |i| i }
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_found_items_are_cached
|
@@ -99,6 +99,12 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|
99
99
|
assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
|
100
100
|
end
|
101
101
|
|
102
|
+
def test_procedural_scopes_returning_nil
|
103
|
+
all_topics = Topic.find(:all)
|
104
|
+
|
105
|
+
assert_equal all_topics, Topic.written_before(nil)
|
106
|
+
end
|
107
|
+
|
102
108
|
def test_scopes_with_joins
|
103
109
|
address = author_addresses(:david_address)
|
104
110
|
posts_with_authors_at_address = Post.find(
|
@@ -247,7 +253,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|
247
253
|
topic = Topic.approved.create!({})
|
248
254
|
assert topic.approved
|
249
255
|
end
|
250
|
-
|
256
|
+
|
251
257
|
def test_should_build_with_proxy_options_chained
|
252
258
|
topic = Topic.approved.by_lifo.build({})
|
253
259
|
assert topic.approved
|
@@ -263,7 +269,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|
263
269
|
end
|
264
270
|
|
265
271
|
def test_should_use_where_in_query_for_named_scope
|
266
|
-
assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises)
|
272
|
+
assert_equal Developer.find_all_by_name('Jamis').to_set, Developer.find_all_by_id(Developer.jamises).to_set
|
267
273
|
end
|
268
274
|
|
269
275
|
def test_size_should_use_count_when_results_are_not_loaded
|
@@ -286,4 +292,68 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|
286
292
|
post = Post.find(1)
|
287
293
|
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
|
288
294
|
end
|
295
|
+
|
296
|
+
def test_chaining_should_use_latest_conditions_when_creating
|
297
|
+
post = Topic.rejected.new
|
298
|
+
assert !post.approved?
|
299
|
+
|
300
|
+
post = Topic.rejected.approved.new
|
301
|
+
assert post.approved?
|
302
|
+
|
303
|
+
post = Topic.approved.rejected.new
|
304
|
+
assert !post.approved?
|
305
|
+
|
306
|
+
post = Topic.approved.rejected.approved.new
|
307
|
+
assert post.approved?
|
308
|
+
end
|
309
|
+
|
310
|
+
def test_chaining_should_use_latest_conditions_when_searching
|
311
|
+
# Normal hash conditions
|
312
|
+
assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all
|
313
|
+
assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all
|
314
|
+
|
315
|
+
# Nested hash conditions with same keys
|
316
|
+
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all
|
317
|
+
|
318
|
+
# Nested hash conditions with different keys
|
319
|
+
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq
|
320
|
+
end
|
321
|
+
|
322
|
+
def test_methods_invoked_within_scopes_should_respect_scope
|
323
|
+
assert_equal [], Topic.approved.by_rejected_ids.proxy_options[:conditions][:id]
|
324
|
+
end
|
325
|
+
|
326
|
+
def test_named_scopes_batch_finders
|
327
|
+
assert_equal 3, Topic.approved.count
|
328
|
+
|
329
|
+
assert_queries(4) do
|
330
|
+
Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
|
331
|
+
end
|
332
|
+
|
333
|
+
assert_queries(2) do
|
334
|
+
Topic.approved.find_in_batches(:batch_size => 2) do |group|
|
335
|
+
group.each {|t| assert t.approved? }
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
class DynamicScopeMatchTest < ActiveRecord::TestCase
|
342
|
+
def test_scoped_by_no_match
|
343
|
+
assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
|
344
|
+
end
|
345
|
+
|
346
|
+
def test_scoped_by
|
347
|
+
match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location")
|
348
|
+
assert_not_nil match
|
349
|
+
assert match.scope?
|
350
|
+
assert_equal %w(age sex location), match.attribute_names
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
class DynamicScopeTest < ActiveRecord::TestCase
|
355
|
+
def test_dynamic_scope
|
356
|
+
assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)
|
357
|
+
assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
|
358
|
+
end
|
289
359
|
end
|
@@ -0,0 +1,509 @@
|
|
1
|
+
require "cases/helper"
|
2
|
+
require "models/pirate"
|
3
|
+
require "models/ship"
|
4
|
+
require "models/bird"
|
5
|
+
require "models/parrot"
|
6
|
+
require "models/treasure"
|
7
|
+
|
8
|
+
module AssertRaiseWithMessage
|
9
|
+
def assert_raise_with_message(expected_exception, expected_message)
|
10
|
+
begin
|
11
|
+
error_raised = false
|
12
|
+
yield
|
13
|
+
rescue expected_exception => error
|
14
|
+
error_raised = true
|
15
|
+
actual_message = error.message
|
16
|
+
end
|
17
|
+
assert error_raised
|
18
|
+
assert_equal expected_message, actual_message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
|
23
|
+
include AssertRaiseWithMessage
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_base_should_have_an_empty_reject_new_nested_attributes_procs
|
30
|
+
assert_equal Hash.new, ActiveRecord::Base.reject_new_nested_attributes_procs
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_should_add_a_proc_to_reject_new_nested_attributes_procs
|
34
|
+
[:parrots, :birds].each do |name|
|
35
|
+
assert_instance_of Proc, Pirate.reject_new_nested_attributes_procs[name]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_should_raise_an_ArgumentError_for_non_existing_associations
|
40
|
+
assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do
|
41
|
+
Pirate.accepts_nested_attributes_for :honesty
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_should_disable_allow_destroy_by_default
|
46
|
+
Pirate.accepts_nested_attributes_for :ship
|
47
|
+
|
48
|
+
pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
|
49
|
+
ship = pirate.create_ship(:name => 'Nights Dirty Lightning')
|
50
|
+
|
51
|
+
assert_no_difference('Ship.count') do
|
52
|
+
pirate.update_attributes(:ship_attributes => { '_delete' => true })
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_a_model_should_respond_to_underscore_delete_and_return_if_it_is_marked_for_destruction
|
57
|
+
ship = Ship.create!(:name => 'Nights Dirty Lightning')
|
58
|
+
assert !ship._delete
|
59
|
+
ship.mark_for_destruction
|
60
|
+
assert ship._delete
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
|
65
|
+
def setup
|
66
|
+
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
|
67
|
+
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_should_define_an_attribute_writer_method_for_the_association
|
71
|
+
assert_respond_to @pirate, :ship_attributes=
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_should_build_a_new_record_if_there_is_no_id
|
75
|
+
@ship.destroy
|
76
|
+
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
|
77
|
+
|
78
|
+
assert @pirate.ship.new_record?
|
79
|
+
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
|
83
|
+
@ship.destroy
|
84
|
+
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
|
85
|
+
|
86
|
+
assert_nil @pirate.ship
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
|
90
|
+
@ship.destroy
|
91
|
+
@pirate.reload.ship_attributes = {}
|
92
|
+
|
93
|
+
assert_nil @pirate.ship
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_should_replace_an_existing_record_if_there_is_no_id
|
97
|
+
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
|
98
|
+
|
99
|
+
assert @pirate.ship.new_record?
|
100
|
+
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
101
|
+
assert_equal 'Nights Dirty Lightning', @ship.name
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
|
105
|
+
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
|
106
|
+
|
107
|
+
assert_equal @ship, @pirate.ship
|
108
|
+
assert_equal 'Nights Dirty Lightning', @pirate.ship.name
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_should_modify_an_existing_record_if_there_is_a_matching_id
|
112
|
+
@pirate.reload.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
|
113
|
+
|
114
|
+
assert_equal @ship, @pirate.ship
|
115
|
+
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
|
119
|
+
@pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' }
|
120
|
+
|
121
|
+
assert_equal @ship, @pirate.ship
|
122
|
+
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
|
126
|
+
@ship.stubs(:id).returns('ABC1X')
|
127
|
+
@pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
|
128
|
+
|
129
|
+
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
|
133
|
+
@pirate.ship.destroy
|
134
|
+
[1, '1', true, 'true'].each do |truth|
|
135
|
+
@pirate.reload.create_ship(:name => 'Mister Pablo')
|
136
|
+
assert_difference('Ship.count', -1) do
|
137
|
+
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_delete => truth })
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
|
143
|
+
[nil, '0', 0, 'false', false].each do |not_truth|
|
144
|
+
assert_no_difference('Ship.count') do
|
145
|
+
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_delete => not_truth })
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
|
151
|
+
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
|
152
|
+
|
153
|
+
assert_no_difference('Ship.count') do
|
154
|
+
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_delete => '1' })
|
155
|
+
end
|
156
|
+
|
157
|
+
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_should_also_work_with_a_HashWithIndifferentAccess
|
161
|
+
@pirate.ship_attributes = HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
|
162
|
+
|
163
|
+
assert !@pirate.ship.new_record?
|
164
|
+
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_should_work_with_update_attributes_as_well
|
168
|
+
@pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :id => @ship.id, :name => 'Mister Pablo' } })
|
169
|
+
@pirate.reload
|
170
|
+
|
171
|
+
assert_equal 'Arr', @pirate.catchphrase
|
172
|
+
assert_equal 'Mister Pablo', @pirate.ship.name
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
|
176
|
+
assert_no_difference('Ship.count') do
|
177
|
+
@pirate.attributes = { :ship_attributes => { :id => @ship.id, :_delete => '1' } }
|
178
|
+
end
|
179
|
+
assert_difference('Ship.count', -1) do
|
180
|
+
@pirate.save
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_should_automatically_enable_autosave_on_the_association
|
185
|
+
assert Pirate.reflect_on_association(:ship).options[:autosave]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
|
190
|
+
def setup
|
191
|
+
@ship = Ship.new(:name => 'Nights Dirty Lightning')
|
192
|
+
@pirate = @ship.build_pirate(:catchphrase => 'Aye')
|
193
|
+
@ship.save!
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_should_define_an_attribute_writer_method_for_the_association
|
197
|
+
assert_respond_to @ship, :pirate_attributes=
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_should_build_a_new_record_if_there_is_no_id
|
201
|
+
@pirate.destroy
|
202
|
+
@ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
|
203
|
+
|
204
|
+
assert @ship.pirate.new_record?
|
205
|
+
assert_equal 'Arr', @ship.pirate.catchphrase
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
|
209
|
+
@pirate.destroy
|
210
|
+
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_delete => '1' }
|
211
|
+
|
212
|
+
assert_nil @ship.pirate
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
|
216
|
+
@pirate.destroy
|
217
|
+
@ship.reload.pirate_attributes = {}
|
218
|
+
|
219
|
+
assert_nil @ship.pirate
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_should_replace_an_existing_record_if_there_is_no_id
|
223
|
+
@ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
|
224
|
+
|
225
|
+
assert @ship.pirate.new_record?
|
226
|
+
assert_equal 'Arr', @ship.pirate.catchphrase
|
227
|
+
assert_equal 'Aye', @pirate.catchphrase
|
228
|
+
end
|
229
|
+
|
230
|
+
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
|
231
|
+
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_delete => '1' }
|
232
|
+
|
233
|
+
assert_equal @pirate, @ship.pirate
|
234
|
+
assert_equal 'Aye', @ship.pirate.catchphrase
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_should_modify_an_existing_record_if_there_is_a_matching_id
|
238
|
+
@ship.reload.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
|
239
|
+
|
240
|
+
assert_equal @pirate, @ship.pirate
|
241
|
+
assert_equal 'Arr', @ship.pirate.catchphrase
|
242
|
+
end
|
243
|
+
|
244
|
+
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
|
245
|
+
@ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' }
|
246
|
+
|
247
|
+
assert_equal @pirate, @ship.pirate
|
248
|
+
assert_equal 'Arr', @ship.pirate.catchphrase
|
249
|
+
end
|
250
|
+
|
251
|
+
def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
|
252
|
+
@pirate.stubs(:id).returns('ABC1X')
|
253
|
+
@ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
|
254
|
+
|
255
|
+
assert_equal 'Arr', @ship.pirate.catchphrase
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
|
259
|
+
@ship.pirate.destroy
|
260
|
+
[1, '1', true, 'true'].each do |truth|
|
261
|
+
@ship.reload.create_pirate(:catchphrase => 'Arr')
|
262
|
+
assert_difference('Pirate.count', -1) do
|
263
|
+
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_delete => truth })
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
|
269
|
+
[nil, '0', 0, 'false', false].each do |not_truth|
|
270
|
+
assert_no_difference('Pirate.count') do
|
271
|
+
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_delete => not_truth })
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
|
277
|
+
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
|
278
|
+
|
279
|
+
assert_no_difference('Pirate.count') do
|
280
|
+
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_delete => '1' })
|
281
|
+
end
|
282
|
+
|
283
|
+
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
284
|
+
end
|
285
|
+
|
286
|
+
def test_should_work_with_update_attributes_as_well
|
287
|
+
@ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
|
288
|
+
@ship.reload
|
289
|
+
|
290
|
+
assert_equal 'Mister Pablo', @ship.name
|
291
|
+
assert_equal 'Arr', @ship.pirate.catchphrase
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
|
295
|
+
assert_no_difference('Pirate.count') do
|
296
|
+
@ship.attributes = { :pirate_attributes => { :id => @ship.pirate.id, '_delete' => true } }
|
297
|
+
end
|
298
|
+
assert_difference('Pirate.count', -1) { @ship.save }
|
299
|
+
end
|
300
|
+
|
301
|
+
def test_should_automatically_enable_autosave_on_the_association
|
302
|
+
assert Ship.reflect_on_association(:pirate).options[:autosave]
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
module NestedAttributesOnACollectionAssociationTests
|
307
|
+
include AssertRaiseWithMessage
|
308
|
+
|
309
|
+
def test_should_define_an_attribute_writer_method_for_the_association
|
310
|
+
assert_respond_to @pirate, association_setter
|
311
|
+
end
|
312
|
+
|
313
|
+
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
|
314
|
+
@alternate_params[association_getter].stringify_keys!
|
315
|
+
@pirate.update_attributes @alternate_params
|
316
|
+
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
|
317
|
+
end
|
318
|
+
|
319
|
+
def test_should_take_an_array_and_assign_the_attributes_to_the_associated_models
|
320
|
+
@pirate.send(association_setter, @alternate_params[association_getter].values)
|
321
|
+
@pirate.save
|
322
|
+
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
|
323
|
+
end
|
324
|
+
|
325
|
+
def test_should_also_work_with_a_HashWithIndifferentAccess
|
326
|
+
@pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
|
327
|
+
@pirate.save
|
328
|
+
assert_equal 'Grace OMalley', @child_1.reload.name
|
329
|
+
end
|
330
|
+
|
331
|
+
def test_should_take_a_hash_and_assign_the_attributes_to_the_associated_models
|
332
|
+
@pirate.attributes = @alternate_params
|
333
|
+
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
|
334
|
+
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
|
335
|
+
end
|
336
|
+
|
337
|
+
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
|
338
|
+
@child_1.stubs(:id).returns('ABC1X')
|
339
|
+
@child_2.stubs(:id).returns('ABC2X')
|
340
|
+
|
341
|
+
@pirate.attributes = {
|
342
|
+
association_getter => [
|
343
|
+
{ :id => @child_1.id, :name => 'Grace OMalley' },
|
344
|
+
{ :id => @child_2.id, :name => 'Privateers Greed' }
|
345
|
+
]
|
346
|
+
}
|
347
|
+
|
348
|
+
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
|
349
|
+
end
|
350
|
+
|
351
|
+
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
|
352
|
+
@pirate.send(@association_name).destroy_all
|
353
|
+
@pirate.reload.attributes = {
|
354
|
+
association_getter => { 'foo' => { :name => 'Grace OMalley' }, 'bar' => { :name => 'Privateers Greed' }}
|
355
|
+
}
|
356
|
+
|
357
|
+
assert @pirate.send(@association_name).first.new_record?
|
358
|
+
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
|
359
|
+
|
360
|
+
assert @pirate.send(@association_name).last.new_record?
|
361
|
+
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
|
362
|
+
end
|
363
|
+
|
364
|
+
def test_should_not_assign_delete_key_to_a_record
|
365
|
+
assert_nothing_raised ActiveRecord::UnknownAttributeError do
|
366
|
+
@pirate.send(association_setter, { 'foo' => { '_delete' => '0' }})
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def test_should_ignore_new_associated_records_with_truthy_delete_attribute
|
371
|
+
@pirate.send(@association_name).destroy_all
|
372
|
+
@pirate.reload.attributes = {
|
373
|
+
association_getter => {
|
374
|
+
'foo' => { :name => 'Grace OMalley' },
|
375
|
+
'bar' => { :name => 'Privateers Greed', '_delete' => '1' }
|
376
|
+
}
|
377
|
+
}
|
378
|
+
|
379
|
+
assert_equal 1, @pirate.send(@association_name).length
|
380
|
+
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
|
381
|
+
end
|
382
|
+
|
383
|
+
def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false
|
384
|
+
@alternate_params[association_getter]['baz'] = {}
|
385
|
+
assert_no_difference("@pirate.send(@association_name).length") do
|
386
|
+
@pirate.attributes = @alternate_params
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models
|
391
|
+
attributes = ActiveSupport::OrderedHash.new
|
392
|
+
attributes['123726353'] = { :name => 'Grace OMalley' }
|
393
|
+
attributes['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353
|
394
|
+
@pirate.send(association_setter, attributes)
|
395
|
+
|
396
|
+
assert_equal ['Posideons Killer', 'Killer bandita Dionne', 'Privateers Greed', 'Grace OMalley'].to_set, @pirate.send(@association_name).map(&:name).to_set
|
397
|
+
end
|
398
|
+
|
399
|
+
def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
|
400
|
+
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
|
401
|
+
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, ActiveSupport::OrderedHash.new) }
|
402
|
+
|
403
|
+
assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do
|
404
|
+
@pirate.send(association_setter, "foo")
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def test_should_work_with_update_attributes_as_well
|
409
|
+
@pirate.update_attributes(:catchphrase => 'Arr',
|
410
|
+
association_getter => { 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' }})
|
411
|
+
|
412
|
+
assert_equal 'Grace OMalley', @child_1.reload.name
|
413
|
+
end
|
414
|
+
|
415
|
+
def test_should_update_existing_records_and_add_new_ones_that_have_no_id
|
416
|
+
@alternate_params[association_getter]['baz'] = { :name => 'Buccaneers Servant' }
|
417
|
+
assert_difference('@pirate.send(@association_name).count', +1) do
|
418
|
+
@pirate.update_attributes @alternate_params
|
419
|
+
end
|
420
|
+
assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
|
421
|
+
end
|
422
|
+
|
423
|
+
def test_should_be_possible_to_destroy_a_record
|
424
|
+
['1', 1, 'true', true].each do |true_variable|
|
425
|
+
record = @pirate.reload.send(@association_name).create!(:name => 'Grace OMalley')
|
426
|
+
@pirate.send(association_setter,
|
427
|
+
@alternate_params[association_getter].merge('baz' => { :id => record.id, '_delete' => true_variable })
|
428
|
+
)
|
429
|
+
|
430
|
+
assert_difference('@pirate.send(@association_name).count', -1) do
|
431
|
+
@pirate.save
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
|
437
|
+
[nil, '', '0', 0, 'false', false].each do |false_variable|
|
438
|
+
@alternate_params[association_getter]['foo']['_delete'] = false_variable
|
439
|
+
assert_no_difference('@pirate.send(@association_name).count') do
|
440
|
+
@pirate.update_attributes(@alternate_params)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
|
446
|
+
assert_no_difference('@pirate.send(@association_name).count') do
|
447
|
+
@pirate.send(association_setter, @alternate_params[association_getter].merge('baz' => { :id => @child_1.id, '_delete' => true }))
|
448
|
+
end
|
449
|
+
assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save }
|
450
|
+
end
|
451
|
+
|
452
|
+
def test_should_automatically_enable_autosave_on_the_association
|
453
|
+
assert Pirate.reflect_on_association(@association_name).options[:autosave]
|
454
|
+
end
|
455
|
+
|
456
|
+
private
|
457
|
+
|
458
|
+
def association_setter
|
459
|
+
@association_setter ||= "#{@association_name}_attributes=".to_sym
|
460
|
+
end
|
461
|
+
|
462
|
+
def association_getter
|
463
|
+
@association_getter ||= "#{@association_name}_attributes".to_sym
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
class TestNestedAttributesOnAHasManyAssociation < ActiveRecord::TestCase
|
468
|
+
def setup
|
469
|
+
@association_type = :has_many
|
470
|
+
@association_name = :birds
|
471
|
+
|
472
|
+
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
|
473
|
+
@pirate.birds.create!(:name => 'Posideons Killer')
|
474
|
+
@pirate.birds.create!(:name => 'Killer bandita Dionne')
|
475
|
+
|
476
|
+
@child_1, @child_2 = @pirate.birds
|
477
|
+
|
478
|
+
@alternate_params = {
|
479
|
+
:birds_attributes => {
|
480
|
+
'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
|
481
|
+
'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
|
482
|
+
}
|
483
|
+
}
|
484
|
+
end
|
485
|
+
|
486
|
+
include NestedAttributesOnACollectionAssociationTests
|
487
|
+
end
|
488
|
+
|
489
|
+
class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
|
490
|
+
def setup
|
491
|
+
@association_type = :has_and_belongs_to_many
|
492
|
+
@association_name = :parrots
|
493
|
+
|
494
|
+
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
|
495
|
+
@pirate.parrots.create!(:name => 'Posideons Killer')
|
496
|
+
@pirate.parrots.create!(:name => 'Killer bandita Dionne')
|
497
|
+
|
498
|
+
@child_1, @child_2 = @pirate.parrots
|
499
|
+
|
500
|
+
@alternate_params = {
|
501
|
+
:parrots_attributes => {
|
502
|
+
'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
|
503
|
+
'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
|
504
|
+
}
|
505
|
+
}
|
506
|
+
end
|
507
|
+
|
508
|
+
include NestedAttributesOnACollectionAssociationTests
|
509
|
+
end
|