paper_trail 3.0.6 → 4.2.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -2
  4. data/.travis.yml +14 -5
  5. data/CHANGELOG.md +215 -8
  6. data/CONTRIBUTING.md +84 -0
  7. data/README.md +922 -502
  8. data/Rakefile +2 -2
  9. data/doc/bug_report_template.rb +65 -0
  10. data/gemfiles/ar3.gemfile +61 -0
  11. data/lib/generators/paper_trail/install_generator.rb +22 -3
  12. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +6 -1
  13. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +11 -0
  14. data/lib/generators/paper_trail/templates/create_version_associations.rb +17 -0
  15. data/lib/generators/paper_trail/templates/create_versions.rb +22 -1
  16. data/lib/paper_trail.rb +52 -22
  17. data/lib/paper_trail/attributes_serialization.rb +89 -0
  18. data/lib/paper_trail/cleaner.rb +32 -15
  19. data/lib/paper_trail/config.rb +35 -2
  20. data/lib/paper_trail/frameworks/active_record.rb +4 -5
  21. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +7 -0
  22. data/lib/paper_trail/frameworks/rails.rb +1 -0
  23. data/lib/paper_trail/frameworks/rails/controller.rb +27 -11
  24. data/lib/paper_trail/frameworks/rspec.rb +5 -0
  25. data/lib/paper_trail/frameworks/sinatra.rb +3 -1
  26. data/lib/paper_trail/has_paper_trail.rb +304 -148
  27. data/lib/paper_trail/record_history.rb +59 -0
  28. data/lib/paper_trail/reifier.rb +270 -0
  29. data/lib/paper_trail/serializers/json.rb +13 -2
  30. data/lib/paper_trail/serializers/yaml.rb +16 -2
  31. data/lib/paper_trail/version_association_concern.rb +15 -0
  32. data/lib/paper_trail/version_concern.rb +160 -122
  33. data/lib/paper_trail/version_number.rb +3 -3
  34. data/paper_trail.gemspec +22 -9
  35. data/spec/generators/install_generator_spec.rb +4 -4
  36. data/spec/models/animal_spec.rb +36 -0
  37. data/spec/models/boolit_spec.rb +48 -0
  38. data/spec/models/callback_modifier_spec.rb +96 -0
  39. data/spec/models/fluxor_spec.rb +19 -0
  40. data/spec/models/gadget_spec.rb +14 -12
  41. data/spec/models/joined_version_spec.rb +9 -9
  42. data/spec/models/json_version_spec.rb +103 -0
  43. data/spec/models/kitchen/banana_spec.rb +14 -0
  44. data/spec/models/not_on_update_spec.rb +19 -0
  45. data/spec/models/post_with_status_spec.rb +3 -3
  46. data/spec/models/skipper_spec.rb +46 -0
  47. data/spec/models/thing_spec.rb +11 -0
  48. data/spec/models/version_spec.rb +195 -44
  49. data/spec/models/widget_spec.rb +136 -76
  50. data/spec/modules/paper_trail_spec.rb +27 -0
  51. data/spec/modules/version_concern_spec.rb +8 -8
  52. data/spec/modules/version_number_spec.rb +16 -16
  53. data/spec/paper_trail/config_spec.rb +52 -0
  54. data/spec/paper_trail_spec.rb +17 -17
  55. data/spec/rails_helper.rb +34 -0
  56. data/spec/requests/articles_spec.rb +10 -14
  57. data/spec/spec_helper.rb +81 -34
  58. data/spec/support/alt_db_init.rb +1 -1
  59. data/test/dummy/app/controllers/application_controller.rb +1 -1
  60. data/test/dummy/app/controllers/articles_controller.rb +4 -1
  61. data/test/dummy/app/models/animal.rb +2 -0
  62. data/test/dummy/app/models/book.rb +4 -0
  63. data/test/dummy/app/models/boolit.rb +4 -0
  64. data/test/dummy/app/models/callback_modifier.rb +45 -0
  65. data/test/dummy/app/models/chapter.rb +9 -0
  66. data/test/dummy/app/models/citation.rb +5 -0
  67. data/test/dummy/app/models/customer.rb +4 -0
  68. data/test/dummy/app/models/editor.rb +4 -0
  69. data/test/dummy/app/models/editorship.rb +5 -0
  70. data/test/dummy/app/models/fruit.rb +5 -0
  71. data/test/dummy/app/models/kitchen/banana.rb +5 -0
  72. data/test/dummy/app/models/line_item.rb +4 -0
  73. data/test/dummy/app/models/not_on_update.rb +4 -0
  74. data/test/dummy/app/models/order.rb +5 -0
  75. data/test/dummy/app/models/paragraph.rb +5 -0
  76. data/test/dummy/app/models/person.rb +13 -3
  77. data/test/dummy/app/models/post.rb +0 -1
  78. data/test/dummy/app/models/quotation.rb +5 -0
  79. data/test/dummy/app/models/section.rb +6 -0
  80. data/test/dummy/app/models/skipper.rb +6 -0
  81. data/test/dummy/app/models/song.rb +20 -0
  82. data/test/dummy/app/models/thing.rb +3 -0
  83. data/test/dummy/app/models/whatchamajigger.rb +4 -0
  84. data/test/dummy/app/models/widget.rb +5 -0
  85. data/test/dummy/app/versions/json_version.rb +3 -0
  86. data/test/dummy/app/versions/kitchen/banana_version.rb +5 -0
  87. data/test/dummy/config/application.rb +6 -0
  88. data/test/dummy/config/database.postgres.yml +1 -1
  89. data/test/dummy/config/environments/test.rb +5 -1
  90. data/test/dummy/config/initializers/paper_trail.rb +6 -1
  91. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +143 -3
  92. data/test/dummy/db/schema.rb +169 -25
  93. data/test/functional/controller_test.rb +4 -2
  94. data/test/functional/modular_sinatra_test.rb +1 -1
  95. data/test/functional/sinatra_test.rb +1 -1
  96. data/test/paper_trail_test.rb +7 -0
  97. data/test/test_helper.rb +38 -2
  98. data/test/time_travel_helper.rb +15 -0
  99. data/test/unit/associations_test.rb +726 -0
  100. data/test/unit/inheritance_column_test.rb +6 -6
  101. data/test/unit/model_test.rb +109 -125
  102. data/test/unit/protected_attrs_test.rb +4 -3
  103. data/test/unit/serializer_test.rb +6 -6
  104. data/test/unit/serializers/json_test.rb +17 -4
  105. data/test/unit/serializers/yaml_test.rb +5 -1
  106. data/test/unit/version_test.rb +87 -69
  107. metadata +172 -75
  108. data/gemfiles/3.0.gemfile +0 -42
  109. data/test/dummy/public/404.html +0 -26
  110. data/test/dummy/public/422.html +0 -26
  111. data/test/dummy/public/500.html +0 -26
  112. data/test/dummy/public/favicon.ico +0 -0
  113. data/test/dummy/public/javascripts/application.js +0 -2
  114. data/test/dummy/public/javascripts/controls.js +0 -965
  115. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  116. data/test/dummy/public/javascripts/effects.js +0 -1123
  117. data/test/dummy/public/javascripts/prototype.js +0 -6001
  118. data/test/dummy/public/javascripts/rails.js +0 -175
  119. data/test/dummy/public/stylesheets/.gitkeep +0 -0
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  # This file is auto-generated from the current state of the database. Instead
2
3
  # of editing this file, please use the migrations feature of Active Record to
3
4
  # incrementally modify your database, and then regenerate this schema definition.
@@ -8,38 +9,133 @@
8
9
  # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
10
  # you'll amass, the slower it'll run and the greater likelihood for issues).
10
11
  #
11
- # It's strongly recommended to check this file into your version control system.
12
+ # It's strongly recommended that you check this file into your version control system.
12
13
 
13
- ActiveRecord::Schema.define(:version => 20110208155312) do
14
+ ActiveRecord::Schema.define(version: 20110208155312) do
14
15
 
15
- create_table "articles", :force => true do |t|
16
+ create_table "animals", force: :cascade do |t|
17
+ t.string "name"
18
+ t.string "species"
19
+ end
20
+
21
+ create_table "articles", force: :cascade do |t|
16
22
  t.string "title"
17
23
  t.string "content"
18
24
  t.string "abstract"
25
+ t.string "file_upload"
19
26
  end
20
27
 
21
- create_table "authorships", :force => true do |t|
28
+ create_table "authorships", force: :cascade do |t|
22
29
  t.integer "book_id"
23
30
  t.integer "person_id"
24
31
  end
25
32
 
26
- create_table "books", :force => true do |t|
33
+ create_table "banana_versions", force: :cascade do |t|
34
+ t.string "item_type", null: false
35
+ t.integer "item_id", null: false
36
+ t.string "event", null: false
37
+ t.string "whodunnit"
38
+ t.text "object"
39
+ t.datetime "created_at"
40
+ end
41
+
42
+ add_index "banana_versions", ["item_type", "item_id"], name: "index_banana_versions_on_item_type_and_item_id"
43
+
44
+ create_table "bananas", force: :cascade do |t|
45
+ t.datetime "created_at"
46
+ t.datetime "updated_at"
47
+ end
48
+
49
+ create_table "books", force: :cascade do |t|
27
50
  t.string "title"
28
51
  end
29
52
 
30
- create_table "fluxors", :force => true do |t|
53
+ create_table "boolits", force: :cascade do |t|
54
+ t.string "name"
55
+ t.boolean "scoped", default: true
56
+ end
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
+
71
+ create_table "customers", force: :cascade do |t|
72
+ t.string "name"
73
+ end
74
+
75
+ create_table "documents", force: :cascade do |t|
76
+ t.string "name"
77
+ end
78
+
79
+ create_table "editors", force: :cascade do |t|
80
+ t.string "name"
81
+ end
82
+
83
+ create_table "editorships", force: :cascade do |t|
84
+ t.integer "book_id"
85
+ t.integer "editor_id"
86
+ end
87
+
88
+ create_table "fluxors", force: :cascade do |t|
31
89
  t.integer "widget_id"
32
90
  t.string "name"
33
91
  end
34
92
 
35
- create_table "people", :force => true do |t|
93
+ create_table "fruits", force: :cascade do |t|
36
94
  t.string "name"
95
+ t.string "color"
96
+ end
97
+
98
+ create_table "gadgets", force: :cascade do |t|
99
+ t.string "name"
100
+ t.string "brand"
101
+ t.datetime "created_at"
102
+ t.datetime "updated_at"
103
+ end
104
+
105
+ create_table "legacy_widgets", force: :cascade do |t|
106
+ t.string "name"
107
+ t.integer "version"
37
108
  end
38
109
 
39
- create_table "post_versions", :force => true do |t|
40
- t.string "item_type", :null => false
41
- t.integer "item_id", :null => false
42
- t.string "event", :null => false
110
+ create_table "line_items", force: :cascade do |t|
111
+ t.integer "order_id"
112
+ t.string "product"
113
+ end
114
+
115
+ create_table "not_on_updates", force: :cascade do |t|
116
+ t.datetime "created_at"
117
+ t.datetime "updated_at"
118
+ end
119
+
120
+ create_table "orders", force: :cascade do |t|
121
+ t.integer "customer_id"
122
+ t.string "order_date"
123
+ end
124
+
125
+ create_table "paragraphs", force: :cascade do |t|
126
+ t.integer "section_id"
127
+ t.string "name"
128
+ end
129
+
130
+ create_table "people", force: :cascade do |t|
131
+ t.string "name"
132
+ t.string "time_zone"
133
+ end
134
+
135
+ create_table "post_versions", force: :cascade do |t|
136
+ t.string "item_type", null: false
137
+ t.integer "item_id", null: false
138
+ t.string "event", null: false
43
139
  t.string "whodunnit"
44
140
  t.text "object"
45
141
  t.datetime "created_at"
@@ -47,52 +143,100 @@ ActiveRecord::Schema.define(:version => 20110208155312) do
47
143
  t.string "user_agent"
48
144
  end
49
145
 
50
- add_index "post_versions", ["item_type", "item_id"], :name => "index_post_versions_on_item_type_and_item_id"
146
+ add_index "post_versions", ["item_type", "item_id"], name: "index_post_versions_on_item_type_and_item_id"
147
+
148
+ create_table "post_with_statuses", force: :cascade do |t|
149
+ t.integer "status"
150
+ end
51
151
 
52
- create_table "posts", :force => true do |t|
152
+ create_table "posts", force: :cascade do |t|
53
153
  t.string "title"
54
154
  t.string "content"
55
155
  end
56
156
 
57
- create_table "songs", :force => true do |t|
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
+
166
+ create_table "skippers", force: :cascade do |t|
167
+ t.string "name"
168
+ t.datetime "another_timestamp"
169
+ t.datetime "created_at"
170
+ t.datetime "updated_at"
171
+ end
172
+
173
+ create_table "songs", force: :cascade do |t|
58
174
  t.integer "length"
59
175
  end
60
176
 
61
- create_table "versions", :force => true do |t|
62
- t.string "item_type", :null => false
63
- t.integer "item_id", :null => false
64
- t.string "event", :null => false
177
+ create_table "things", force: :cascade do |t|
178
+ t.string "name"
179
+ end
180
+
181
+ create_table "translations", force: :cascade do |t|
182
+ t.string "headline"
183
+ t.string "content"
184
+ t.string "language_code"
185
+ t.string "type"
186
+ end
187
+
188
+ create_table "version_associations", force: :cascade do |t|
189
+ t.integer "version_id"
190
+ t.string "foreign_key_name", null: false
191
+ t.integer "foreign_key_id"
192
+ end
193
+
194
+ add_index "version_associations", ["foreign_key_name", "foreign_key_id"], name: "index_version_associations_on_foreign_key"
195
+ add_index "version_associations", ["version_id"], name: "index_version_associations_on_version_id"
196
+
197
+ create_table "versions", force: :cascade do |t|
198
+ t.string "item_type", null: false
199
+ t.integer "item_id", null: false
200
+ t.string "event", null: false
65
201
  t.string "whodunnit"
66
202
  t.text "object"
203
+ t.text "object_changes"
204
+ t.integer "transaction_id"
67
205
  t.datetime "created_at"
68
206
  t.integer "answer"
69
207
  t.string "action"
70
208
  t.string "question"
71
209
  t.integer "article_id"
210
+ t.string "title"
72
211
  t.string "ip"
73
212
  t.string "user_agent"
74
- t.text :object_changes
75
213
  end
76
214
 
77
- add_index "versions", ["item_type", "item_id"], :name => "index_versions_on_item_type_and_item_id"
215
+ add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
216
+
217
+ create_table "whatchamajiggers", force: :cascade do |t|
218
+ t.string "owner_type"
219
+ t.integer "owner_id"
220
+ t.string "name"
221
+ end
78
222
 
79
- create_table "widgets", :force => true do |t|
223
+ create_table "widgets", force: :cascade do |t|
80
224
  t.string "name"
81
225
  t.text "a_text"
82
226
  t.integer "an_integer"
83
227
  t.float "a_float"
84
- t.decimal "a_decimal"
228
+ t.decimal "a_decimal", precision: 6, scale: 4
85
229
  t.datetime "a_datetime"
86
230
  t.time "a_time"
87
231
  t.date "a_date"
88
232
  t.boolean "a_boolean"
89
- t.datetime "created_at"
90
- t.datetime "updated_at"
91
233
  t.string "sacrificial_column"
92
234
  t.string "type"
235
+ t.datetime "created_at"
236
+ t.datetime "updated_at"
93
237
  end
94
238
 
95
- create_table "wotsits", :force => true do |t|
239
+ create_table "wotsits", force: :cascade do |t|
96
240
  t.integer "widget_id"
97
241
  t.string "name"
98
242
  t.datetime "created_at"
@@ -7,8 +7,10 @@ class ControllerTest < ActionController::TestCase
7
7
  @request.env['REMOTE_ADDR'] = '127.0.0.1'
8
8
  end
9
9
 
10
+ # Mimick what RequestStore will do outside of the test env, since it is
11
+ # middleware, and doesn't get executed in controller / request specs
10
12
  teardown do
11
- PaperTrail.enabled_for_controller = true
13
+ RequestStore.store[:paper_trail] = nil
12
14
  end
13
15
 
14
16
  test 'disable on create' do
@@ -82,7 +84,7 @@ class ControllerTest < ActionController::TestCase
82
84
  @request.env['HTTP_USER_AGENT'] = 'Disable User-Agent'
83
85
  post :create, :widget => { :name => 'Flugel' }
84
86
  assert_equal 0, assigns(:widget).versions.length
85
- assert !PaperTrail.enabled_for_controller?
87
+ assert !PaperTrail.enabled_for_controller?
86
88
  assert PaperTrail.whodunnit.nil?
87
89
  assert PaperTrail.controller_info.nil?
88
90
  end
@@ -16,7 +16,7 @@ class BaseApp < Sinatra::Base
16
16
  def current_user
17
17
  @current_user ||= OpenStruct.new(:id => 'foobar').tap do |obj|
18
18
  # Invoking `id` returns the `object_id` value in Ruby18 unless specifically overwritten
19
- def obj.id; 'foobar'; end if RUBY_VERSION.to_f < 1.9
19
+ def obj.id; 'foobar'; end if RUBY_VERSION < '1.9'
20
20
  end
21
21
  end
22
22
  end
@@ -16,7 +16,7 @@ class Sinatra::Application
16
16
  def current_user
17
17
  @current_user ||= OpenStruct.new(:id => 'raboof').tap do |obj|
18
18
  # Invoking `id` returns the `object_id` value in Ruby18 unless specifically overwritten
19
- def obj.id; 'raboof'; end if RUBY_VERSION.to_f < 1.9
19
+ def obj.id; 'raboof'; end if RUBY_VERSION < '1.9'
20
20
  end
21
21
  end
22
22
 
@@ -8,6 +8,13 @@ class PaperTrailTest < ActiveSupport::TestCase
8
8
  test 'Version Number' do
9
9
  assert PaperTrail.const_defined?(:VERSION)
10
10
  end
11
+
12
+ test 'enabled is thread-safe' do
13
+ Thread.new do
14
+ PaperTrail.enabled = false
15
+ end.join
16
+ assert PaperTrail.enabled?
17
+ end
11
18
 
12
19
  test 'create with plain model class' do
13
20
  widget = Widget.create
data/test/test_helper.rb CHANGED
@@ -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
 
@@ -13,7 +19,7 @@ require File.expand_path("../dummy/config/environment.rb", __FILE__)
13
19
  require "rails/test_help"
14
20
  require 'shoulda'
15
21
  require 'ffaker'
16
- require 'database_cleaner' if using_mysql?
22
+ require 'database_cleaner'
17
23
 
18
24
  Rails.backtrace_cleaner.remove_silencers!
19
25
 
@@ -24,7 +30,7 @@ ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__
24
30
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
25
31
 
26
32
  # DatabaseCleaner is apparently necessary for doing proper transactions within MySQL (ugh)
27
- DatabaseCleaner.strategy = :truncation if using_mysql?
33
+ DatabaseCleaner.strategy = :truncation
28
34
 
29
35
  # global setup block resetting Thread.current
30
36
  class ActiveSupport::TestCase
@@ -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,15 @@
1
+ if RUBY_VERSION < "1.9.2"
2
+ require 'delorean'
3
+
4
+ class Timecop
5
+ def self.travel(t)
6
+ Delorean.time_travel_to t
7
+ end
8
+
9
+ def self.return
10
+ Delorean.back_to_the_present
11
+ end
12
+ end
13
+ else
14
+ require 'timecop'
15
+ end
@@ -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