paper_trail 3.0.6 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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