paper_trail 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +27 -0
  4. data/CONTRIBUTING.md +78 -5
  5. data/README.md +328 -268
  6. data/doc/bug_report_template.rb +65 -0
  7. data/gemfiles/3.0.gemfile +7 -4
  8. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +1 -1
  9. data/lib/generators/paper_trail/templates/create_versions.rb +14 -0
  10. data/lib/paper_trail.rb +11 -9
  11. data/lib/paper_trail/attributes_serialization.rb +89 -0
  12. data/lib/paper_trail/cleaner.rb +8 -1
  13. data/lib/paper_trail/config.rb +15 -18
  14. data/lib/paper_trail/frameworks/rails/controller.rb +16 -2
  15. data/lib/paper_trail/has_paper_trail.rb +102 -99
  16. data/lib/paper_trail/record_history.rb +59 -0
  17. data/lib/paper_trail/reifier.rb +270 -0
  18. data/lib/paper_trail/version_association_concern.rb +3 -1
  19. data/lib/paper_trail/version_concern.rb +60 -226
  20. data/lib/paper_trail/version_number.rb +2 -2
  21. data/paper_trail.gemspec +7 -10
  22. data/spec/models/animal_spec.rb +17 -0
  23. data/spec/models/callback_modifier_spec.rb +96 -0
  24. data/spec/models/json_version_spec.rb +20 -17
  25. data/spec/paper_trail/config_spec.rb +52 -0
  26. data/spec/spec_helper.rb +6 -0
  27. data/test/dummy/app/models/callback_modifier.rb +45 -0
  28. data/test/dummy/app/models/chapter.rb +9 -0
  29. data/test/dummy/app/models/citation.rb +5 -0
  30. data/test/dummy/app/models/paragraph.rb +5 -0
  31. data/test/dummy/app/models/quotation.rb +5 -0
  32. data/test/dummy/app/models/section.rb +6 -0
  33. data/test/dummy/config/database.postgres.yml +1 -1
  34. data/test/dummy/config/initializers/paper_trail.rb +3 -1
  35. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +33 -0
  36. data/test/dummy/db/schema.rb +27 -0
  37. data/test/test_helper.rb +36 -0
  38. data/test/unit/associations_test.rb +726 -0
  39. data/test/unit/inheritance_column_test.rb +6 -6
  40. data/test/unit/model_test.rb +62 -594
  41. data/test/unit/protected_attrs_test.rb +3 -2
  42. data/test/unit/version_test.rb +87 -69
  43. metadata +38 -2
@@ -55,6 +55,19 @@ ActiveRecord::Schema.define(version: 20110208155312) do
55
55
  t.boolean "scoped", default: true
56
56
  end
57
57
 
58
+ create_table "callback_modifiers", force: :cascade do |t|
59
+ t.string "some_content"
60
+ t.boolean "deleted", default: false
61
+ end
62
+
63
+ create_table "chapters", force: :cascade do |t|
64
+ t.string "name"
65
+ end
66
+
67
+ create_table "citations", force: :cascade do |t|
68
+ t.integer "quotation_id"
69
+ end
70
+
58
71
  create_table "customers", force: :cascade do |t|
59
72
  t.string "name"
60
73
  end
@@ -109,6 +122,11 @@ ActiveRecord::Schema.define(version: 20110208155312) do
109
122
  t.string "order_date"
110
123
  end
111
124
 
125
+ create_table "paragraphs", force: :cascade do |t|
126
+ t.integer "section_id"
127
+ t.string "name"
128
+ end
129
+
112
130
  create_table "people", force: :cascade do |t|
113
131
  t.string "name"
114
132
  t.string "time_zone"
@@ -136,6 +154,15 @@ ActiveRecord::Schema.define(version: 20110208155312) do
136
154
  t.string "content"
137
155
  end
138
156
 
157
+ create_table "quotations", force: :cascade do |t|
158
+ t.integer "chapter_id"
159
+ end
160
+
161
+ create_table "sections", force: :cascade do |t|
162
+ t.integer "chapter_id"
163
+ t.string "name"
164
+ end
165
+
139
166
  create_table "skippers", force: :cascade do |t|
140
167
  t.string "name"
141
168
  t.datetime "another_timestamp"
@@ -1,3 +1,9 @@
1
+ begin
2
+ require 'pry-nav'
3
+ rescue LoadError
4
+ # It's OK, we don't include pry in e.g. gemfiles/3.0.gemfile
5
+ end
6
+
1
7
  ENV["RAILS_ENV"] = "test"
2
8
  ENV["DB"] ||= "sqlite"
3
9
 
@@ -37,6 +43,36 @@ class ActiveSupport::TestCase
37
43
  DatabaseCleaner.clean if using_mysql?
38
44
  Thread.current[:paper_trail] = nil
39
45
  end
46
+
47
+ private
48
+
49
+ def assert_attributes_equal(expected, actual)
50
+ if using_mysql?
51
+ expected, actual = expected.dup, actual.dup
52
+
53
+ # Adjust timestamps for missing fractional seconds precision.
54
+ %w(created_at updated_at).each do |timestamp|
55
+ expected[timestamp] = expected[timestamp].change(:usec => 0)
56
+ actual[timestamp] = actual[timestamp].change(:usec => 0)
57
+ end
58
+ end
59
+
60
+ assert_equal expected, actual
61
+ end
62
+
63
+ def assert_changes_equal(expected, actual)
64
+ if using_mysql?
65
+ expected, actual = expected.dup, actual.dup
66
+
67
+ # Adjust timestamps for missing fractional seconds precision.
68
+ %w(created_at updated_at).each do |timestamp|
69
+ expected[timestamp][1] = expected[timestamp][1].change(:usec => 0)
70
+ actual[timestamp][1] = actual[timestamp][1].change(:usec => 0)
71
+ end
72
+ end
73
+
74
+ assert_equal expected, actual
75
+ end
40
76
  end
41
77
 
42
78
  #
@@ -0,0 +1,726 @@
1
+ require 'test_helper'
2
+ require 'time_travel_helper'
3
+
4
+ class AssociationsTest < ActiveSupport::TestCase
5
+ CHAPTER_NAMES = [
6
+ "Down the Rabbit-Hole",
7
+ "The Pool of Tears",
8
+ "A Caucus-Race and a Long Tale",
9
+ "The Rabbit Sends in a Little Bill",
10
+ "Advice from a Caterpillar",
11
+ "Pig and Pepper",
12
+ "A Mad Tea-Party",
13
+ "The Queen's Croquet-Ground",
14
+ "The Mock Turtle's Story",
15
+ "The Lobster Quadrille",
16
+ "Who Stole the Tarts?",
17
+ "Alice's Evidence"
18
+ ]
19
+
20
+ # These would have been done in test_helper.rb if using_mysql? is true
21
+ unless using_mysql?
22
+ self.use_transactional_fixtures = false
23
+ setup { DatabaseCleaner.start }
24
+ end
25
+
26
+ teardown do
27
+ Timecop.return
28
+ # This would have been done in test_helper.rb if using_mysql? is true
29
+ DatabaseCleaner.clean unless using_mysql?
30
+ end
31
+
32
+ context "a has_one association" do
33
+ setup { @widget = Widget.create :name => 'widget_0' }
34
+
35
+ context 'before the associated was created' do
36
+ setup do
37
+ @widget.update_attributes :name => 'widget_1'
38
+ @wotsit = @widget.create_wotsit :name => 'wotsit_0'
39
+ end
40
+
41
+ context 'when reified' do
42
+ setup { @widget_0 = @widget.versions.last.reify(:has_one => true) }
43
+
44
+ should 'see the associated as it was at the time' do
45
+ assert_nil @widget_0.wotsit
46
+ end
47
+
48
+ should 'not persist changes to the live association' do
49
+ assert_equal @wotsit, @widget.wotsit(true)
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'where the association is created between model versions' do
55
+ setup do
56
+ @wotsit = @widget.create_wotsit :name => 'wotsit_0'
57
+ Timecop.travel 1.second.since
58
+ @widget.update_attributes :name => 'widget_1'
59
+ end
60
+
61
+ context 'when reified' do
62
+ setup { @widget_0 = @widget.versions.last.reify(:has_one => true) }
63
+
64
+ should 'see the associated as it was at the time' do
65
+ assert_equal 'wotsit_0', @widget_0.wotsit.name
66
+ end
67
+
68
+ should 'not persist changes to the live association' do
69
+ assert_equal @wotsit, @widget.wotsit(true)
70
+ end
71
+ end
72
+
73
+ context 'and then the associated is updated between model versions' do
74
+ setup do
75
+ @wotsit.update_attributes :name => 'wotsit_1'
76
+ @wotsit.update_attributes :name => 'wotsit_2'
77
+ Timecop.travel 1.second.since
78
+ @widget.update_attributes :name => 'widget_2'
79
+ @wotsit.update_attributes :name => 'wotsit_3'
80
+ end
81
+
82
+ context 'when reified' do
83
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => true) }
84
+
85
+ should 'see the associated as it was at the time' do
86
+ assert_equal 'wotsit_2', @widget_1.wotsit.name
87
+ end
88
+
89
+ should 'not persist changes to the live association' do
90
+ assert_equal 'wotsit_3', @widget.wotsit(true).name
91
+ end
92
+ end
93
+
94
+ context 'when reified opting out of has_one reification' do
95
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => false) }
96
+
97
+ should 'see the associated as it is live' do
98
+ assert_equal 'wotsit_3', @widget_1.wotsit.name
99
+ end
100
+ end
101
+ end
102
+
103
+ context 'and then the associated is destroyed' do
104
+ setup do
105
+ @wotsit.destroy
106
+ end
107
+
108
+ context 'when reify' do
109
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => true) }
110
+
111
+ should 'see the associated as it was at the time' do
112
+ assert_equal @wotsit, @widget_1.wotsit
113
+ end
114
+
115
+ should 'not persist changes to the live association' do
116
+ assert_nil @widget.wotsit(true)
117
+ end
118
+ end
119
+
120
+ context 'and then the model is updated' do
121
+ setup do
122
+ Timecop.travel 1.second.since
123
+ @widget.update_attributes :name => 'widget_3'
124
+ end
125
+
126
+ context 'when reified' do
127
+ setup { @widget_2 = @widget.versions.last.reify(:has_one => true) }
128
+
129
+ should 'see the associated as it was at the time' do
130
+ assert_nil @widget_2.wotsit
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ context "a has_many association" do
139
+ setup { @customer = Customer.create :name => 'customer_0' }
140
+
141
+ context 'updated before the associated was created' do
142
+ setup do
143
+ @customer.update_attributes! :name => 'customer_1'
144
+ @customer.orders.create! :order_date => Date.today
145
+ end
146
+
147
+ context 'when reified' do
148
+ setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }
149
+
150
+ should 'see the associated as it was at the time' do
151
+ assert_equal [], @customer_0.orders
152
+ end
153
+
154
+ should 'not persist changes to the live association' do
155
+ assert_not_equal [], @customer.orders(true)
156
+ end
157
+ end
158
+
159
+ context 'when reified with option mark_for_destruction' do
160
+ setup { @customer_0 = @customer.versions.last.reify(:has_many => true, :mark_for_destruction => true) }
161
+
162
+ should 'mark the associated for destruction' do
163
+ assert_equal [true], @customer_0.orders.map(&:marked_for_destruction?)
164
+ end
165
+ end
166
+ end
167
+
168
+ context 'where the association is created between model versions' do
169
+ setup do
170
+ @order = @customer.orders.create! :order_date => 'order_date_0'
171
+ Timecop.travel 1.second.since
172
+ @customer.update_attributes :name => 'customer_1'
173
+ end
174
+
175
+ context 'when reified' do
176
+ setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }
177
+
178
+ should 'see the associated as it was at the time' do
179
+ assert_equal ['order_date_0'], @customer_0.orders.map(&:order_date)
180
+ end
181
+ end
182
+
183
+ context 'and then a nested has_many association is created' do
184
+ setup do
185
+ @order.line_items.create! :product => 'product_0'
186
+ end
187
+
188
+ context 'when reified' do
189
+ setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }
190
+
191
+ should 'see the live version of the nested association' do
192
+ assert_equal ['product_0'], @customer_0.orders.first.line_items.map(&:product)
193
+ end
194
+ end
195
+ end
196
+
197
+ context 'and then the associated is updated between model versions' do
198
+ setup do
199
+ @order.update_attributes :order_date => 'order_date_1'
200
+ @order.update_attributes :order_date => 'order_date_2'
201
+ Timecop.travel 1.second.since
202
+ @customer.update_attributes :name => 'customer_2'
203
+ @order.update_attributes :order_date => 'order_date_3'
204
+ end
205
+
206
+ context 'when reified' do
207
+ setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }
208
+
209
+ should 'see the associated as it was at the time' do
210
+ assert_equal ['order_date_2'], @customer_1.orders.map(&:order_date)
211
+ end
212
+
213
+ should 'not persist changes to the live association' do
214
+ assert_equal ['order_date_3'], @customer.orders(true).map(&:order_date)
215
+ end
216
+ end
217
+
218
+ context 'when reified opting out of has_many reification' do
219
+ setup { @customer_1 = @customer.versions.last.reify(:has_many => false) }
220
+
221
+ should 'see the associated as it is live' do
222
+ assert_equal ['order_date_3'], @customer_1.orders.map(&:order_date)
223
+ end
224
+ end
225
+
226
+ context 'and then the associated is destroyed' do
227
+ setup do
228
+ @order.destroy
229
+ end
230
+
231
+ context 'when reified' do
232
+ setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }
233
+
234
+ should 'see the associated as it was at the time' do
235
+ assert_equal ['order_date_2'], @customer_1.orders.map(&:order_date)
236
+ end
237
+
238
+ should 'not persist changes to the live association' do
239
+ assert_equal [], @customer.orders(true)
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ context 'and then the associated is destroyed' do
246
+ setup do
247
+ @order.destroy
248
+ end
249
+
250
+ context 'when reified' do
251
+ setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }
252
+
253
+ should 'see the associated as it was at the time' do
254
+ assert_equal [@order.order_date], @customer_1.orders.map(&:order_date)
255
+ end
256
+
257
+ should 'not persist changes to the live association' do
258
+ assert_equal [], @customer.orders(true)
259
+ end
260
+ end
261
+ end
262
+
263
+ context 'and then the associated is destroyed between model versions' do
264
+ setup do
265
+ @order.destroy
266
+ Timecop.travel 1.second.since
267
+ @customer.update_attributes :name => 'customer_2'
268
+ end
269
+
270
+ context 'when reified' do
271
+ setup { @customer_1 = @customer.versions.last.reify(:has_many => true) }
272
+
273
+ should 'see the associated as it was at the time' do
274
+ assert_equal [], @customer_1.orders
275
+ end
276
+ end
277
+ end
278
+
279
+ context 'and then another association is added' do
280
+ setup do
281
+ @customer.orders.create! :order_date => 'order_date_1'
282
+ end
283
+
284
+ context 'when reified' do
285
+ setup { @customer_0 = @customer.versions.last.reify(:has_many => true) }
286
+
287
+ should 'see the associated as it was at the time' do
288
+ assert_equal ['order_date_0'], @customer_0.orders.map(&:order_date)
289
+ end
290
+
291
+ should 'not persist changes to the live association' do
292
+ assert_equal ['order_date_0', 'order_date_1'], @customer.orders(true).map(&:order_date).sort
293
+ end
294
+ end
295
+
296
+ context 'when reified with option mark_for_destruction' do
297
+ setup { @customer_0 = @customer.versions.last.reify(:has_many => true, :mark_for_destruction => true) }
298
+
299
+ should 'mark the newly associated for destruction' do
300
+ assert @customer_0.orders.detect { |o| o.order_date == 'order_date_1'}.marked_for_destruction?
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+
307
+ context "has_many through associations" do
308
+ context "Books, Authors, and Authorships" do
309
+ setup { @book = Book.create :title => 'book_0' }
310
+
311
+ context 'updated before the associated was created' do
312
+ setup do
313
+ @book.update_attributes! :title => 'book_1'
314
+ @book.authors.create! :name => 'author_0'
315
+ end
316
+
317
+ context 'when reified' do
318
+ setup { @book_0 = @book.versions.last.reify(:has_many => true) }
319
+
320
+ should 'see the associated as it was at the time' do
321
+ assert_equal [], @book_0.authors
322
+ end
323
+
324
+ should 'not persist changes to the live association' do
325
+ assert_equal ['author_0'], @book.authors(true).map(&:name)
326
+ end
327
+ end
328
+
329
+ context 'when reified with option mark_for_destruction' do
330
+ setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }
331
+
332
+ should 'mark the associated for destruction' do
333
+ assert_equal [true], @book_0.authors.map(&:marked_for_destruction?)
334
+ end
335
+
336
+ should 'mark the associated-through for destruction' do
337
+ assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
338
+ end
339
+ end
340
+ end
341
+
342
+ context 'updated before it is associated with an existing one' do
343
+ setup do
344
+ person_existing = Person.create(:name => 'person_existing')
345
+ Timecop.travel 1.second.since
346
+ @book.update_attributes! :title => 'book_1'
347
+ @book.authors << person_existing
348
+ end
349
+
350
+ context 'when reified' do
351
+ setup { @book_0 = @book.versions.last.reify(:has_many => true) }
352
+
353
+ should 'see the associated as it was at the time' do
354
+ assert_equal [], @book_0.authors
355
+ end
356
+ end
357
+
358
+ context 'when reified with option mark_for_destruction' do
359
+ setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }
360
+
361
+ should 'not mark the associated for destruction' do
362
+ assert_equal [false], @book_0.authors.map(&:marked_for_destruction?)
363
+ end
364
+
365
+ should 'mark the associated-through for destruction' do
366
+ assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
367
+ end
368
+ end
369
+ end
370
+
371
+ context 'where the association is created between model versions' do
372
+ setup do
373
+ @author = @book.authors.create! :name => 'author_0'
374
+ @person_existing = Person.create(:name => 'person_existing')
375
+ Timecop.travel 1.second.since
376
+ @book.update_attributes! :title => 'book_1'
377
+ end
378
+
379
+ context 'when reified' do
380
+ setup { @book_0 = @book.versions.last.reify(:has_many => true) }
381
+
382
+ should 'see the associated as it was at the time' do
383
+ assert_equal ['author_0'], @book_0.authors.map(&:name)
384
+ end
385
+ end
386
+
387
+ context 'and then the associated is updated between model versions' do
388
+ setup do
389
+ @author.update_attributes :name => 'author_1'
390
+ @author.update_attributes :name => 'author_2'
391
+ Timecop.travel 1.second.since
392
+ @book.update_attributes :title => 'book_2'
393
+ @author.update_attributes :name => 'author_3'
394
+ end
395
+
396
+ context 'when reified' do
397
+ setup { @book_1 = @book.versions.last.reify(:has_many => true) }
398
+
399
+ should 'see the associated as it was at the time' do
400
+ assert_equal ['author_2'], @book_1.authors.map(&:name)
401
+ end
402
+
403
+ should 'not persist changes to the live association' do
404
+ assert_equal ['author_3'], @book.authors(true).map(&:name)
405
+ end
406
+ end
407
+
408
+ context 'when reified opting out of has_many reification' do
409
+ setup { @book_1 = @book.versions.last.reify(:has_many => false) }
410
+
411
+ should 'see the associated as it is live' do
412
+ assert_equal ['author_3'], @book_1.authors.map(&:name)
413
+ end
414
+ end
415
+ end
416
+
417
+ context 'and then the associated is destroyed' do
418
+ setup do
419
+ @author.destroy
420
+ end
421
+
422
+ context 'when reified' do
423
+ setup { @book_1 = @book.versions.last.reify(:has_many => true) }
424
+
425
+ should 'see the associated as it was at the time' do
426
+ assert_equal [@author.name], @book_1.authors.map(&:name)
427
+ end
428
+
429
+ should 'not persist changes to the live association' do
430
+ assert_equal [], @book.authors(true)
431
+ end
432
+ end
433
+ end
434
+
435
+ context 'and then the associated is destroyed between model versions' do
436
+ setup do
437
+ @author.destroy
438
+ Timecop.travel 1.second.since
439
+ @book.update_attributes :title => 'book_2'
440
+ end
441
+
442
+ context 'when reified' do
443
+ setup { @book_1 = @book.versions.last.reify(:has_many => true) }
444
+
445
+ should 'see the associated as it was at the time' do
446
+ assert_equal [], @book_1.authors
447
+ end
448
+ end
449
+ end
450
+
451
+ context 'and then the associated is dissociated between model versions' do
452
+ setup do
453
+ @book.authors = []
454
+ Timecop.travel 1.second.since
455
+ @book.update_attributes :title => 'book_2'
456
+ end
457
+
458
+ context 'when reified' do
459
+ setup { @book_1 = @book.versions.last.reify(:has_many => true) }
460
+
461
+ should 'see the associated as it was at the time' do
462
+ assert_equal [], @book_1.authors
463
+ end
464
+ end
465
+ end
466
+
467
+ context 'and then another associated is created' do
468
+ setup do
469
+ @book.authors.create! :name => 'author_1'
470
+ end
471
+
472
+ context 'when reified' do
473
+ setup { @book_0 = @book.versions.last.reify(:has_many => true) }
474
+
475
+ should 'only see the first associated' do
476
+ assert_equal ['author_0'], @book_0.authors.map(&:name)
477
+ end
478
+
479
+ should 'not persist changes to the live association' do
480
+ assert_equal ['author_0', 'author_1'], @book.authors(true).map(&:name)
481
+ end
482
+ end
483
+
484
+ context 'when reified with option mark_for_destruction' do
485
+ setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }
486
+
487
+ should 'mark the newly associated for destruction' do
488
+ assert @book_0.authors.detect { |a| a.name == 'author_1' }.marked_for_destruction?
489
+ end
490
+
491
+ should 'mark the newly associated-through for destruction' do
492
+ assert @book_0.authorships.detect { |as| as.person.name == 'author_1' }.marked_for_destruction?
493
+ end
494
+ end
495
+ end
496
+
497
+ context 'and then an existing one is associated' do
498
+ setup do
499
+ @book.authors << @person_existing
500
+ end
501
+
502
+ context 'when reified' do
503
+ setup { @book_0 = @book.versions.last.reify(:has_many => true) }
504
+
505
+ should 'only see the first associated' do
506
+ assert_equal ['author_0'], @book_0.authors.map(&:name)
507
+ end
508
+
509
+ should 'not persist changes to the live association' do
510
+ assert_equal ['author_0', 'person_existing'], @book.authors(true).map(&:name).sort
511
+ end
512
+ end
513
+
514
+ context 'when reified with option mark_for_destruction' do
515
+ setup { @book_0 = @book.versions.last.reify(:has_many => true, :mark_for_destruction => true) }
516
+
517
+ should 'not mark the newly associated for destruction' do
518
+ assert !@book_0.authors.detect { |a| a.name == 'person_existing' }.marked_for_destruction?
519
+ end
520
+
521
+ should 'mark the newly associated-through for destruction' do
522
+ assert @book_0.authorships.detect { |as| as.person.name == 'person_existing' }.marked_for_destruction?
523
+ end
524
+ end
525
+ end
526
+ end
527
+
528
+ context 'updated before the associated without paper_trail was created' do
529
+ setup do
530
+ @book.update_attributes! :title => 'book_1'
531
+ @book.editors.create! :name => 'editor_0'
532
+ end
533
+
534
+ context 'when reified' do
535
+ setup { @book_0 = @book.versions.last.reify(:has_many => true) }
536
+
537
+ should 'see the live association' do
538
+ assert_equal ['editor_0'], @book_0.editors.map(&:name)
539
+ end
540
+ end
541
+ end
542
+ end
543
+
544
+ context "Chapters, Sections, Paragraphs, Quotations, and Citations" do
545
+ setup { @chapter = Chapter.create(:name => CHAPTER_NAMES[0]) }
546
+
547
+ context "before any associations are created" do
548
+ setup do
549
+ @chapter.update_attributes(:name => CHAPTER_NAMES[1])
550
+ end
551
+
552
+ should "not reify any associations" do
553
+ chapter_v1 = @chapter.versions[1].reify(:has_many => true)
554
+ assert_equal CHAPTER_NAMES[0], chapter_v1.name
555
+ assert_equal [], chapter_v1.sections
556
+ assert_equal [], chapter_v1.paragraphs
557
+ end
558
+ end
559
+
560
+ context "after the first has_many through relationship is created" do
561
+ setup do
562
+ assert_equal 1, @chapter.versions.size
563
+ @chapter.update_attributes :name => CHAPTER_NAMES[1]
564
+ assert_equal 2, @chapter.versions.size
565
+
566
+ Timecop.travel 1.second.since
567
+ @chapter.sections.create :name => "section 1"
568
+ Timecop.travel 1.second.since
569
+ @chapter.sections.first.update_attributes :name => "section 2"
570
+ Timecop.travel 1.second.since
571
+ @chapter.update_attributes :name => CHAPTER_NAMES[2]
572
+ assert_equal 3, @chapter.versions.size
573
+
574
+ Timecop.travel 1.second.since
575
+ @chapter.sections.first.update_attributes :name => "section 3"
576
+ end
577
+
578
+ context "version 1" do
579
+ should "have no sections" do
580
+ chapter_v1 = @chapter.versions[1].reify(:has_many => true)
581
+ assert_equal [], chapter_v1.sections
582
+ end
583
+ end
584
+
585
+ context "version 2" do
586
+ should "have one section" do
587
+ chapter_v2 = @chapter.versions[2].reify(:has_many => true)
588
+ assert_equal 1, chapter_v2.sections.size
589
+
590
+ # Shows the value of the section as it was before
591
+ # the chapter was updated.
592
+ assert_equal ['section 2'], chapter_v2.sections.map(&:name)
593
+
594
+ # Shows the value of the chapter as it was before
595
+ assert_equal CHAPTER_NAMES[1], chapter_v2.name
596
+ end
597
+ end
598
+
599
+ context "version 2, before the section was destroyed" do
600
+ setup do
601
+ @chapter.update_attributes :name => CHAPTER_NAMES[2]
602
+ Timecop.travel 1.second.since
603
+ @chapter.sections.destroy_all
604
+ Timecop.travel 1.second.since
605
+ end
606
+
607
+ should "have the one section" do
608
+ chapter_v2 = @chapter.versions[2].reify(:has_many => true)
609
+ assert_equal ['section 2'], chapter_v2.sections.map(&:name)
610
+ end
611
+ end
612
+
613
+ context "version 3, after the section was destroyed" do
614
+ setup do
615
+ @chapter.sections.destroy_all
616
+ Timecop.travel 1.second.since
617
+ @chapter.update_attributes :name => CHAPTER_NAMES[3]
618
+ Timecop.travel 1.second.since
619
+ end
620
+
621
+ should "have no sections" do
622
+ chapter_v3 = @chapter.versions[3].reify(:has_many => true)
623
+ assert_equal 0, chapter_v3.sections.size
624
+ end
625
+ end
626
+
627
+ context "after creating a paragraph" do
628
+ setup do
629
+ assert_equal 3, @chapter.versions.size
630
+ @section = @chapter.sections.first
631
+ Timecop.travel 1.second.since
632
+ @paragraph = @section.paragraphs.create :name => 'para1'
633
+ end
634
+
635
+ context "new chapter version" do
636
+ should "have one paragraph" do
637
+ initial_section_name = @section.name
638
+ initial_paragraph_name = @paragraph.name
639
+ Timecop.travel 1.second.since
640
+ @chapter.update_attributes :name => CHAPTER_NAMES[4]
641
+ assert_equal 4, @chapter.versions.size
642
+ Timecop.travel 1.second.since
643
+ @paragraph.update_attributes :name => 'para3'
644
+ chapter_v3 = @chapter.versions[3].reify(:has_many => true)
645
+ assert_equal [initial_section_name], chapter_v3.sections.map(&:name)
646
+ paragraphs = chapter_v3.sections.first.paragraphs
647
+ assert_equal 1, paragraphs.size
648
+ assert_equal [initial_paragraph_name], paragraphs.map(&:name)
649
+ end
650
+ end
651
+
652
+ context "the version before a section is destroyed" do
653
+ should "have the section and paragraph" do
654
+ Timecop.travel 1.second.since
655
+ @chapter.update_attributes(:name => CHAPTER_NAMES[3])
656
+ assert_equal 4, @chapter.versions.size
657
+ Timecop.travel 1.second.since
658
+ @section.destroy
659
+ assert_equal 4, @chapter.versions.size
660
+ chapter_v3 = @chapter.versions[3].reify(:has_many => true)
661
+ assert_equal CHAPTER_NAMES[2], chapter_v3.name
662
+ assert_equal [@section], chapter_v3.sections
663
+ assert_equal [@paragraph], chapter_v3.sections[0].paragraphs
664
+ assert_equal [@paragraph], chapter_v3.paragraphs
665
+ end
666
+ end
667
+
668
+ context "the version after a section is destroyed" do
669
+ should "not have any sections or paragraphs" do
670
+ @section.destroy
671
+ Timecop.travel 1.second.since
672
+ @chapter.update_attributes(:name => CHAPTER_NAMES[5])
673
+ assert_equal 4, @chapter.versions.size
674
+ chapter_v3 = @chapter.versions[3].reify(:has_many => true)
675
+ assert_equal 0, chapter_v3.sections.size
676
+ assert_equal 0, chapter_v3.paragraphs.size
677
+ end
678
+ end
679
+
680
+ context "the version before a paragraph is destroyed" do
681
+ should "have the one paragraph" do
682
+ initial_paragraph_name = @section.paragraphs.first.name
683
+ Timecop.travel 1.second.since
684
+ @chapter.update_attributes(:name => CHAPTER_NAMES[5])
685
+ Timecop.travel 1.second.since
686
+ @paragraph.destroy
687
+ chapter_v3 = @chapter.versions[3].reify(:has_many => true)
688
+ paragraphs = chapter_v3.sections.first.paragraphs
689
+ assert_equal 1, paragraphs.size
690
+ assert_equal initial_paragraph_name, paragraphs.first.name
691
+ end
692
+ end
693
+
694
+ context "the version after a paragraph is destroyed" do
695
+ should "have no paragraphs" do
696
+ @paragraph.destroy
697
+ Timecop.travel 1.second.since
698
+ @chapter.update_attributes(:name => CHAPTER_NAMES[5])
699
+ chapter_v3 = @chapter.versions[3].reify(:has_many => true)
700
+ assert_equal 0, chapter_v3.paragraphs.size
701
+ assert_equal [], chapter_v3.sections.first.paragraphs
702
+ end
703
+ end
704
+ end
705
+ end
706
+
707
+ context "a chapter with one paragraph and one citation" do
708
+ should "reify paragraphs and citations" do
709
+ chapter = Chapter.create(:name => CHAPTER_NAMES[0])
710
+ section = Section.create(:name => 'Section One', :chapter => chapter)
711
+ paragraph = Paragraph.create(:name => 'Paragraph One', :section => section)
712
+ quotation = Quotation.create(:chapter => chapter)
713
+ citation = Citation.create(:quotation => quotation)
714
+ Timecop.travel 1.second.since
715
+ chapter.update_attributes(:name => CHAPTER_NAMES[1])
716
+ assert_equal 2, chapter.versions.count
717
+ paragraph.destroy
718
+ citation.destroy
719
+ reified = chapter.versions[1].reify(:has_many => true)
720
+ assert_equal [paragraph], reified.sections.first.paragraphs
721
+ assert_equal [citation], reified.quotations.first.citations
722
+ end
723
+ end
724
+ end
725
+ end
726
+ end