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.

Files changed (120) hide show
  1. data/CHANGELOG +438 -396
  2. data/Rakefile +4 -2
  3. data/lib/active_record.rb +46 -43
  4. data/lib/active_record/association_preload.rb +34 -19
  5. data/lib/active_record/associations.rb +193 -251
  6. data/lib/active_record/associations/association_collection.rb +38 -21
  7. data/lib/active_record/associations/association_proxy.rb +11 -4
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
  9. data/lib/active_record/associations/has_many_association.rb +2 -2
  10. data/lib/active_record/associations/has_many_through_association.rb +8 -8
  11. data/lib/active_record/associations/has_one_association.rb +11 -2
  12. data/lib/active_record/attribute_methods.rb +1 -0
  13. data/lib/active_record/autosave_association.rb +349 -0
  14. data/lib/active_record/base.rb +292 -106
  15. data/lib/active_record/batches.rb +73 -0
  16. data/lib/active_record/calculations.rb +34 -16
  17. data/lib/active_record/callbacks.rb +37 -8
  18. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +16 -0
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -0
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +103 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -6
  22. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +28 -25
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -5
  24. data/lib/active_record/connection_adapters/mysql_adapter.rb +50 -21
  25. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -41
  26. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
  27. data/lib/active_record/connection_adapters/sqlite_adapter.rb +41 -21
  28. data/lib/active_record/dirty.rb +1 -1
  29. data/lib/active_record/dynamic_scope_match.rb +25 -0
  30. data/lib/active_record/fixtures.rb +193 -198
  31. data/lib/active_record/locale/en.yml +1 -1
  32. data/lib/active_record/locking/optimistic.rb +33 -0
  33. data/lib/active_record/migration.rb +8 -2
  34. data/lib/active_record/named_scope.rb +13 -6
  35. data/lib/active_record/nested_attributes.rb +329 -0
  36. data/lib/active_record/query_cache.rb +25 -13
  37. data/lib/active_record/reflection.rb +6 -1
  38. data/lib/active_record/schema_dumper.rb +2 -0
  39. data/lib/active_record/serialization.rb +3 -1
  40. data/lib/active_record/serializers/json_serializer.rb +19 -0
  41. data/lib/active_record/serializers/xml_serializer.rb +28 -13
  42. data/lib/active_record/session_store.rb +318 -0
  43. data/lib/active_record/test_case.rb +15 -9
  44. data/lib/active_record/timestamp.rb +2 -2
  45. data/lib/active_record/transactions.rb +58 -8
  46. data/lib/active_record/validations.rb +29 -24
  47. data/lib/active_record/version.rb +2 -2
  48. data/test/cases/ar_schema_test.rb +0 -1
  49. data/test/cases/associations/belongs_to_associations_test.rb +35 -131
  50. data/test/cases/associations/cascaded_eager_loading_test.rb +8 -0
  51. data/test/cases/associations/eager_load_nested_include_test.rb +29 -0
  52. data/test/cases/associations/eager_test.rb +137 -7
  53. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +45 -7
  54. data/test/cases/associations/has_many_associations_test.rb +110 -149
  55. data/test/cases/associations/has_many_through_associations_test.rb +39 -7
  56. data/test/cases/associations/has_one_associations_test.rb +39 -92
  57. data/test/cases/associations/has_one_through_associations_test.rb +34 -3
  58. data/test/cases/associations/inner_join_association_test.rb +0 -5
  59. data/test/cases/associations/join_model_test.rb +5 -7
  60. data/test/cases/attribute_methods_test.rb +13 -1
  61. data/test/cases/autosave_association_test.rb +901 -0
  62. data/test/cases/base_test.rb +41 -21
  63. data/test/cases/batches_test.rb +61 -0
  64. data/test/cases/calculations_test.rb +37 -17
  65. data/test/cases/callbacks_test.rb +43 -5
  66. data/test/cases/connection_pool_test.rb +25 -0
  67. data/test/cases/copy_table_test_sqlite.rb +11 -0
  68. data/test/cases/datatype_test_postgresql.rb +1 -0
  69. data/test/cases/defaults_test.rb +37 -26
  70. data/test/cases/dirty_test.rb +26 -2
  71. data/test/cases/finder_test.rb +79 -44
  72. data/test/cases/fixtures_test.rb +15 -19
  73. data/test/cases/helper.rb +26 -19
  74. data/test/cases/inheritance_test.rb +2 -2
  75. data/test/cases/json_serialization_test.rb +1 -1
  76. data/test/cases/locking_test.rb +23 -5
  77. data/test/cases/method_scoping_test.rb +126 -3
  78. data/test/cases/migration_test.rb +253 -237
  79. data/test/cases/named_scope_test.rb +73 -3
  80. data/test/cases/nested_attributes_test.rb +509 -0
  81. data/test/cases/query_cache_test.rb +0 -4
  82. data/test/cases/reflection_test.rb +13 -3
  83. data/test/cases/reload_models_test.rb +3 -1
  84. data/test/cases/repair_helper.rb +50 -0
  85. data/test/cases/schema_dumper_test.rb +0 -1
  86. data/test/cases/transactions_test.rb +177 -12
  87. data/test/cases/validations_i18n_test.rb +288 -294
  88. data/test/cases/validations_test.rb +230 -180
  89. data/test/cases/xml_serialization_test.rb +19 -1
  90. data/test/fixtures/fixture_database.sqlite3 +0 -0
  91. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  92. data/test/fixtures/member_types.yml +6 -0
  93. data/test/fixtures/members.yml +3 -1
  94. data/test/fixtures/people.yml +10 -1
  95. data/test/fixtures/toys.yml +4 -0
  96. data/test/models/author.rb +1 -2
  97. data/test/models/bird.rb +3 -0
  98. data/test/models/category.rb +1 -0
  99. data/test/models/company.rb +3 -0
  100. data/test/models/developer.rb +12 -0
  101. data/test/models/event.rb +3 -0
  102. data/test/models/member.rb +1 -0
  103. data/test/models/member_detail.rb +1 -0
  104. data/test/models/member_type.rb +3 -0
  105. data/test/models/owner.rb +2 -1
  106. data/test/models/parrot.rb +2 -0
  107. data/test/models/person.rb +6 -0
  108. data/test/models/pet.rb +2 -1
  109. data/test/models/pirate.rb +55 -1
  110. data/test/models/post.rb +6 -0
  111. data/test/models/project.rb +1 -0
  112. data/test/models/reply.rb +6 -0
  113. data/test/models/ship.rb +8 -1
  114. data/test/models/ship_part.rb +5 -0
  115. data/test/models/topic.rb +13 -1
  116. data/test/models/toy.rb +4 -0
  117. data/test/schema/schema.rb +35 -2
  118. metadata +70 -9
  119. data/test/fixtures/fixture_database.sqlite +0 -0
  120. 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.each { |i| i }
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