paper_trail 6.0.2 → 7.0.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +20 -0
  3. data/.rubocop.yml +30 -2
  4. data/.rubocop_todo.yml +20 -0
  5. data/.travis.yml +3 -5
  6. data/Appraisals +5 -6
  7. data/CHANGELOG.md +33 -0
  8. data/README.md +43 -81
  9. data/Rakefile +1 -1
  10. data/doc/bug_report_template.rb +4 -2
  11. data/gemfiles/ar_4.0.gemfile +7 -0
  12. data/gemfiles/ar_4.2.gemfile +0 -1
  13. data/lib/generators/paper_trail/templates/create_version_associations.rb +1 -1
  14. data/lib/generators/paper_trail/templates/create_versions.rb +1 -1
  15. data/lib/paper_trail.rb +7 -9
  16. data/lib/paper_trail/config.rb +0 -15
  17. data/lib/paper_trail/frameworks/rspec.rb +8 -2
  18. data/lib/paper_trail/model_config.rb +6 -2
  19. data/lib/paper_trail/record_trail.rb +3 -1
  20. data/lib/paper_trail/reifier.rb +43 -354
  21. data/lib/paper_trail/reifiers/belongs_to.rb +48 -0
  22. data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +50 -0
  23. data/lib/paper_trail/reifiers/has_many.rb +110 -0
  24. data/lib/paper_trail/reifiers/has_many_through.rb +90 -0
  25. data/lib/paper_trail/reifiers/has_one.rb +76 -0
  26. data/lib/paper_trail/serializers/yaml.rb +2 -25
  27. data/lib/paper_trail/version_concern.rb +5 -5
  28. data/lib/paper_trail/version_number.rb +7 -3
  29. data/paper_trail.gemspec +7 -34
  30. data/spec/controllers/articles_controller_spec.rb +1 -1
  31. data/spec/generators/install_generator_spec.rb +40 -34
  32. data/spec/models/animal_spec.rb +50 -25
  33. data/spec/models/boolit_spec.rb +8 -7
  34. data/spec/models/callback_modifier_spec.rb +13 -13
  35. data/spec/models/document_spec.rb +21 -0
  36. data/spec/models/gadget_spec.rb +35 -39
  37. data/spec/models/joined_version_spec.rb +4 -4
  38. data/spec/models/json_version_spec.rb +14 -15
  39. data/spec/models/not_on_update_spec.rb +1 -1
  40. data/spec/models/post_with_status_spec.rb +2 -2
  41. data/spec/models/skipper_spec.rb +4 -4
  42. data/spec/models/thing_spec.rb +1 -1
  43. data/spec/models/truck_spec.rb +1 -1
  44. data/spec/models/vehicle_spec.rb +1 -1
  45. data/spec/models/version_spec.rb +152 -168
  46. data/spec/models/widget_spec.rb +170 -196
  47. data/spec/modules/paper_trail_spec.rb +3 -3
  48. data/spec/modules/version_concern_spec.rb +5 -8
  49. data/spec/modules/version_number_spec.rb +11 -36
  50. data/spec/paper_trail/cleaner_spec.rb +152 -0
  51. data/spec/paper_trail/config_spec.rb +1 -1
  52. data/spec/paper_trail/serializers/custom_yaml_serializer_spec.rb +45 -0
  53. data/spec/paper_trail/serializers/json_spec.rb +57 -0
  54. data/spec/paper_trail/version_limit_spec.rb +55 -0
  55. data/spec/paper_trail_spec.rb +45 -32
  56. data/spec/requests/articles_spec.rb +4 -4
  57. data/test/dummy/app/models/custom_primary_key_record.rb +4 -2
  58. data/test/dummy/app/models/document.rb +1 -1
  59. data/test/dummy/app/models/not_on_update.rb +1 -1
  60. data/test/dummy/app/models/on/create.rb +6 -0
  61. data/test/dummy/app/models/on/destroy.rb +6 -0
  62. data/test/dummy/app/models/on/empty_array.rb +6 -0
  63. data/test/dummy/app/models/on/update.rb +6 -0
  64. data/test/dummy/app/models/person.rb +1 -0
  65. data/test/dummy/app/models/song.rb +19 -28
  66. data/test/dummy/config/application.rb +10 -43
  67. data/test/dummy/config/routes.rb +1 -1
  68. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +25 -51
  69. data/test/dummy/db/schema.rb +29 -19
  70. data/test/test_helper.rb +0 -16
  71. data/test/unit/associations_test.rb +81 -81
  72. data/test/unit/model_test.rb +48 -131
  73. data/test/unit/serializer_test.rb +34 -45
  74. data/test/unit/serializers/mixin_json_test.rb +3 -1
  75. data/test/unit/serializers/yaml_test.rb +1 -5
  76. metadata +44 -19
  77. data/lib/paper_trail/frameworks/sinatra.rb +0 -40
  78. data/test/functional/modular_sinatra_test.rb +0 -46
  79. data/test/functional/sinatra_test.rb +0 -51
  80. data/test/unit/cleaner_test.rb +0 -151
  81. data/test/unit/inheritance_column_test.rb +0 -41
  82. data/test/unit/serializers/json_test.rb +0 -95
  83. data/test/unit/serializers/mixin_yaml_test.rb +0 -53
@@ -38,65 +38,63 @@ describe Widget, type: :model do
38
38
 
39
39
  describe "versioning option" do
40
40
  context "enabled", versioning: true do
41
- it "should enable versioning" do
41
+ it "enables versioning" do
42
42
  expect(widget.versions.size).to eq(1)
43
43
  end
44
44
  end
45
45
 
46
46
  context "disabled (default)" do
47
- it "should not enable versioning" do
47
+ it "does not enable versioning" do
48
48
  expect(widget.versions.size).to eq(0)
49
49
  end
50
50
  end
51
51
  end
52
52
 
53
53
  describe "Callbacks", versioning: true do
54
- describe :before_save do
55
- context ":on => :update" do
56
- before { widget.update_attributes!(name: "Foobar") }
54
+ describe "before_save" do
55
+ before { widget.update_attributes!(name: "Foobar") }
57
56
 
58
- subject { widget.versions.last.reify }
57
+ subject { widget.versions.last.reify }
59
58
 
60
- it "resets value for timestamp attrs for update so that value gets updated properly" do
61
- # Travel 1 second because MySQL lacks sub-second resolution
62
- Timecop.travel(1) do
63
- expect { subject.save! }.to change(subject, :updated_at)
64
- end
59
+ it "resets value for timestamp attrs for update so that value gets updated properly" do
60
+ # Travel 1 second because MySQL lacks sub-second resolution
61
+ Timecop.travel(1) do
62
+ expect { subject.save! }.to change(subject, :updated_at)
65
63
  end
66
64
  end
67
65
  end
68
66
 
69
- describe :after_create do
67
+ describe "after_create" do
70
68
  let(:widget) { Widget.create!(name: "Foobar", created_at: Time.now - 1.week) }
71
69
 
72
- it "corresponding version should use the widget's `updated_at`" do
70
+ it "corresponding version uses the widget's `updated_at`" do
73
71
  expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
74
72
  end
75
73
  end
76
74
 
77
- describe :after_update do
75
+ describe "after_update" do
78
76
  before { widget.update_attributes!(name: "Foobar", updated_at: Time.now + 1.week) }
79
77
 
80
78
  subject { widget.versions.last.reify }
81
79
 
82
80
  it { expect(subject.paper_trail).not_to be_live }
83
81
 
84
- it "should clear the `versions_association_name` virtual attribute" do
82
+ it "clears the `versions_association_name` virtual attribute" do
85
83
  subject.save!
86
84
  expect(subject.paper_trail).to be_live
87
85
  end
88
86
 
89
- it "corresponding version should use the widget updated_at" do
87
+ it "corresponding version uses the widget updated_at" do
90
88
  expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
91
89
  end
92
90
  end
93
91
 
94
- describe :after_destroy do
95
- it "should create a version for that event" do
92
+ describe "after_destroy" do
93
+ it "creates a version for that event" do
96
94
  expect { widget.destroy }.to change(widget.versions, :count).by(1)
97
95
  end
98
96
 
99
- it "should assign the version into the `versions_association_name`" do
97
+ it "assigns the version into the `versions_association_name`" do
100
98
  expect(widget.version).to be_nil
101
99
  widget.destroy
102
100
  expect(widget.version).not_to be_nil
@@ -104,7 +102,7 @@ describe Widget, type: :model do
104
102
  end
105
103
  end
106
104
 
107
- describe :after_rollback do
105
+ describe "after_rollback" do
108
106
  let(:rolled_back_name) { "Big Moo" }
109
107
 
110
108
  before do
@@ -122,19 +120,19 @@ describe Widget, type: :model do
122
120
 
123
121
  it "does not create an event for changes that did not happen" do
124
122
  widget.versions.map(&:changeset).each do |changeset|
125
- expect(changeset.fetch("name", [])).to_not include(rolled_back_name)
123
+ expect(changeset.fetch("name", [])).not_to include(rolled_back_name)
126
124
  end
127
125
  end
128
126
 
129
127
  it "has not yet loaded the assocation" do
130
- expect(widget.versions).to_not be_loaded
128
+ expect(widget.versions).not_to be_loaded
131
129
  end
132
130
  end
133
131
  end
134
132
 
135
133
  describe "Association", versioning: true do
136
134
  describe "sort order" do
137
- it "should sort by the timestamp order from the `VersionConcern`" do
135
+ it "sorts by the timestamp order from the `VersionConcern`" do
138
136
  expect(widget.versions.to_sql).to eq(
139
137
  widget.versions.reorder(PaperTrail::Version.timestamp_sort_order).to_sql
140
138
  )
@@ -144,7 +142,7 @@ describe Widget, type: :model do
144
142
 
145
143
  if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
146
144
  describe "IdentityMap", versioning: true do
147
- it "should not clobber the IdentityMap when reifying" do
145
+ it "does not clobber the IdentityMap when reifying" do
148
146
  widget.update_attributes name: "Henry", created_at: Time.now - 1.day
149
147
  widget.update_attributes name: "Harry"
150
148
  expect(ActiveRecord::IdentityMap).to receive(:without).once
@@ -153,204 +151,180 @@ describe Widget, type: :model do
153
151
  end
154
152
  end
155
153
 
156
- describe "Methods" do
157
- describe "Instance", versioning: true do
158
- describe "#create" do
159
- it "creates a version record" do
160
- wordget = Widget.create
161
- assert_equal 1, wordget.versions.length
162
- end
163
- end
164
-
165
- describe "#destroy" do
166
- it "creates a version record" do
167
- widget = Widget.create
168
- assert_equal 1, widget.versions.length
169
- widget.destroy
170
- versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id)
171
- assert_equal 2, versions_for_widget.length
172
- end
173
-
174
- it "can have multiple destruction records" do
175
- versions = lambda { |widget|
176
- # Workaround for AR 3. When we drop AR 3 support, we can simply use
177
- # the `widget.versions` association, instead of `with_item_keys`.
178
- PaperTrail::Version.with_item_keys("Widget", widget.id)
179
- }
180
- widget = Widget.create
181
- assert_equal 1, widget.versions.length
182
- widget.destroy
183
- assert_equal 2, versions.call(widget).length
184
- widget = widget.version.reify
185
- widget.save
186
- assert_equal 3, versions.call(widget).length
187
- widget.destroy
188
- assert_equal 4, versions.call(widget).length
189
- assert_equal 2, versions.call(widget).where(event: "destroy").length
190
- end
191
- end
192
-
193
- describe "#paper_trail.originator" do
194
- describe "return value" do
195
- let(:orig_name) { FFaker::Name.name }
196
- let(:new_name) { FFaker::Name.name }
197
- before { PaperTrail.whodunnit = orig_name }
198
-
199
- context "accessed from live model instance" do
200
- specify { expect(widget.paper_trail).to be_live }
201
-
202
- it "should return the originator for the model at a given state" do
203
- expect(widget.paper_trail.originator).to eq(orig_name)
204
- widget.paper_trail.whodunnit(new_name) { |w|
205
- w.update_attributes(name: "Elizabeth")
206
- }
207
- expect(widget.paper_trail.originator).to eq(new_name)
208
- end
209
- end
210
-
211
- context "accessed from a reified model instance" do
212
- before do
213
- widget.update_attributes(name: "Andy")
214
- PaperTrail.whodunnit = new_name
215
- widget.update_attributes(name: "Elizabeth")
216
- end
217
-
218
- context "default behavior (no `options[:dup]` option passed in)" do
219
- let(:reified_widget) { widget.versions[1].reify }
220
-
221
- it "should return the appropriate originator" do
222
- expect(reified_widget.paper_trail.originator).to eq(orig_name)
223
- end
154
+ describe "#create", versioning: true do
155
+ it "creates a version record" do
156
+ wordget = Widget.create
157
+ assert_equal 1, wordget.versions.length
158
+ end
159
+ end
224
160
 
225
- it "should not create a new model instance" do
226
- expect(reified_widget).not_to be_new_record
227
- end
228
- end
161
+ describe "#destroy", versioning: true do
162
+ it "creates a version record" do
163
+ widget = Widget.create
164
+ assert_equal 1, widget.versions.length
165
+ widget.destroy
166
+ versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id)
167
+ assert_equal 2, versions_for_widget.length
168
+ end
229
169
 
230
- context "creating a new instance (`options[:dup] == true`)" do
231
- let(:reified_widget) { widget.versions[1].reify(dup: true) }
170
+ it "can have multiple destruction records" do
171
+ versions = lambda { |widget|
172
+ # Workaround for AR 3. When we drop AR 3 support, we can simply use
173
+ # the `widget.versions` association, instead of `with_item_keys`.
174
+ PaperTrail::Version.with_item_keys("Widget", widget.id)
175
+ }
176
+ widget = Widget.create
177
+ assert_equal 1, widget.versions.length
178
+ widget.destroy
179
+ assert_equal 2, versions.call(widget).length
180
+ widget = widget.version.reify
181
+ widget.save
182
+ assert_equal 3, versions.call(widget).length
183
+ widget.destroy
184
+ assert_equal 4, versions.call(widget).length
185
+ assert_equal 2, versions.call(widget).where(event: "destroy").length
186
+ end
187
+ end
232
188
 
233
- it "should return the appropriate originator" do
234
- expect(reified_widget.paper_trail.originator).to eq(orig_name)
235
- end
189
+ describe "#paper_trail.originator", versioning: true do
190
+ describe "return value" do
191
+ let(:orig_name) { FFaker::Name.name }
192
+ let(:new_name) { FFaker::Name.name }
236
193
 
237
- it "should not create a new model instance" do
238
- expect(reified_widget).to be_new_record
239
- end
240
- end
241
- end
242
- end
194
+ before do
195
+ PaperTrail.whodunnit = orig_name
243
196
  end
244
197
 
245
- describe "#version_at" do
246
- context "Timestamp argument is AFTER object has been destroyed" do
247
- it "should return `nil`" do
248
- widget.update_attribute(:name, "foobar")
249
- widget.destroy
250
- expect(widget.paper_trail.version_at(Time.now)).to be_nil
251
- end
252
- end
198
+ it "returns the originator for the model at a given state" do
199
+ expect(widget.paper_trail).to be_live
200
+ expect(widget.paper_trail.originator).to eq(orig_name)
201
+ widget.paper_trail.whodunnit(new_name) { |w|
202
+ w.update_attributes(name: "Elizabeth")
203
+ }
204
+ expect(widget.paper_trail.originator).to eq(new_name)
253
205
  end
254
206
 
255
- describe "#whodunnit" do
256
- context "no block given" do
257
- it "should raise an error" do
258
- expect {
259
- widget.paper_trail.whodunnit("Ben")
260
- }.to raise_error(ArgumentError, "expected to receive a block")
261
- end
262
- end
207
+ it "returns the appropriate originator" do
208
+ widget.update_attributes(name: "Andy")
209
+ PaperTrail.whodunnit = new_name
210
+ widget.update_attributes(name: "Elizabeth")
211
+ reified_widget = widget.versions[1].reify
212
+ expect(reified_widget.paper_trail.originator).to eq(orig_name)
213
+ expect(reified_widget).not_to be_new_record
214
+ end
263
215
 
264
- context "block given" do
265
- let(:orig_name) { FFaker::Name.name }
266
- let(:new_name) { FFaker::Name.name }
216
+ it "can create a new instance with options[:dup]" do
217
+ widget.update_attributes(name: "Andy")
218
+ PaperTrail.whodunnit = new_name
219
+ widget.update_attributes(name: "Elizabeth")
220
+ reified_widget = widget.versions[1].reify(dup: true)
221
+ expect(reified_widget.paper_trail.originator).to eq(orig_name)
222
+ expect(reified_widget).to be_new_record
223
+ end
224
+ end
225
+ end
267
226
 
268
- before do
269
- PaperTrail.whodunnit = orig_name
270
- expect(widget.versions.last.whodunnit).to eq(orig_name) # persist `widget`
271
- end
227
+ describe "#version_at", versioning: true do
228
+ context "Timestamp argument is AFTER object has been destroyed" do
229
+ it "returns nil" do
230
+ widget.update_attribute(:name, "foobar")
231
+ widget.destroy
232
+ expect(widget.paper_trail.version_at(Time.now)).to be_nil
233
+ end
234
+ end
235
+ end
272
236
 
273
- it "should modify value of `PaperTrail.whodunnit` while executing the block" do
274
- widget.paper_trail.whodunnit(new_name) do
275
- expect(PaperTrail.whodunnit).to eq(new_name)
276
- widget.update_attributes(name: "Elizabeth")
277
- end
278
- expect(widget.versions.last.whodunnit).to eq(new_name)
279
- end
237
+ describe "#whodunnit", versioning: true do
238
+ context "no block given" do
239
+ it "raises an error" do
240
+ expect {
241
+ widget.paper_trail.whodunnit("Ben")
242
+ }.to raise_error(ArgumentError, "expected to receive a block")
243
+ end
244
+ end
280
245
 
281
- context "after executing the block" do
282
- it "reverts value of whodunnit to previous value" do
283
- widget.paper_trail.whodunnit(new_name) { |w|
284
- w.update_attributes(name: "Elizabeth")
285
- }
286
- expect(PaperTrail.whodunnit).to eq(orig_name)
287
- end
288
- end
246
+ context "block given" do
247
+ let(:orig_name) { FFaker::Name.name }
248
+ let(:new_name) { FFaker::Name.name }
289
249
 
290
- context "error within block" do
291
- it "still reverts the whodunnit value to previous value" do
292
- expect {
293
- widget.paper_trail.whodunnit(new_name) { raise }
294
- }.to raise_error(RuntimeError)
295
- expect(PaperTrail.whodunnit).to eq(orig_name)
296
- end
297
- end
298
- end
250
+ before do
251
+ PaperTrail.whodunnit = orig_name
252
+ expect(widget.versions.last.whodunnit).to eq(orig_name) # persist `widget`
299
253
  end
300
254
 
301
- describe "#touch_with_version" do
302
- it "creates a version" do
303
- count = widget.versions.size
304
- # Travel 1 second because MySQL lacks sub-second resolution
305
- Timecop.travel(1) do
306
- widget.paper_trail.touch_with_version
307
- end
308
- expect(widget.versions.size).to eq(count + 1)
255
+ it "modifies value of `PaperTrail.whodunnit` while executing the block" do
256
+ widget.paper_trail.whodunnit(new_name) do
257
+ expect(PaperTrail.whodunnit).to eq(new_name)
258
+ widget.update_attributes(name: "Elizabeth")
309
259
  end
260
+ expect(widget.versions.last.whodunnit).to eq(new_name)
261
+ end
310
262
 
311
- it "increments the `:updated_at` timestamp" do
312
- time_was = widget.updated_at
313
- # Travel 1 second because MySQL lacks sub-second resolution
314
- Timecop.travel(1) do
315
- widget.paper_trail.touch_with_version
316
- end
317
- expect(widget.updated_at).to be > time_was
318
- end
263
+ it "reverts value of whodunnit to previous value after executing the block" do
264
+ widget.paper_trail.whodunnit(new_name) { |w|
265
+ w.update_attributes(name: "Elizabeth")
266
+ }
267
+ expect(PaperTrail.whodunnit).to eq(orig_name)
319
268
  end
320
269
 
321
- describe "#update" do
322
- it "creates a version record" do
323
- widget = Widget.create
324
- assert_equal 1, widget.versions.length
325
- widget.update_attributes(name: "Bugle")
326
- assert_equal 2, widget.versions.length
327
- end
270
+ it "reverts to previous value, even if error within block" do
271
+ expect {
272
+ widget.paper_trail.whodunnit(new_name) { raise }
273
+ }.to raise_error(RuntimeError)
274
+ expect(PaperTrail.whodunnit).to eq(orig_name)
328
275
  end
329
276
  end
277
+ end
330
278
 
331
- describe "Class" do
332
- describe ".paper_trail.enabled?" do
333
- it "returns true" do
334
- expect(Widget.paper_trail.enabled?).to eq(true)
335
- end
279
+ describe "#touch_with_version", versioning: true do
280
+ it "creates a version" do
281
+ count = widget.versions.size
282
+ # Travel 1 second because MySQL lacks sub-second resolution
283
+ Timecop.travel(1) do
284
+ widget.paper_trail.touch_with_version
336
285
  end
286
+ expect(widget.versions.size).to eq(count + 1)
287
+ end
337
288
 
338
- describe ".disable" do
339
- it "should set the `paper_trail.enabled?` to `false`" do
340
- expect(Widget.paper_trail.enabled?).to eq(true)
341
- Widget.paper_trail.disable
342
- expect(Widget.paper_trail.enabled?).to eq(false)
343
- end
289
+ it "increments the `:updated_at` timestamp" do
290
+ time_was = widget.updated_at
291
+ # Travel 1 second because MySQL lacks sub-second resolution
292
+ Timecop.travel(1) do
293
+ widget.paper_trail.touch_with_version
344
294
  end
295
+ expect(widget.updated_at).to be > time_was
296
+ end
297
+ end
345
298
 
346
- describe ".enable" do
347
- it "should set the `paper_trail.enabled?` to `true`" do
348
- Widget.paper_trail.disable
349
- expect(Widget.paper_trail.enabled?).to eq(false)
350
- Widget.paper_trail.enable
351
- expect(Widget.paper_trail.enabled?).to eq(true)
352
- end
353
- end
299
+ describe "#update", versioning: true do
300
+ it "creates a version record" do
301
+ widget = Widget.create
302
+ assert_equal 1, widget.versions.length
303
+ widget.update_attributes(name: "Bugle")
304
+ assert_equal 2, widget.versions.length
305
+ end
306
+ end
307
+
308
+ describe ".paper_trail.enabled?" do
309
+ it "returns true" do
310
+ expect(Widget.paper_trail.enabled?).to eq(true)
311
+ end
312
+ end
313
+
314
+ describe ".disable" do
315
+ it "sets the `paper_trail.enabled?` to `false`" do
316
+ expect(Widget.paper_trail.enabled?).to eq(true)
317
+ Widget.paper_trail.disable
318
+ expect(Widget.paper_trail.enabled?).to eq(false)
319
+ end
320
+ end
321
+
322
+ describe ".enable" do
323
+ it "sets the `paper_trail.enabled?` to `true`" do
324
+ Widget.paper_trail.disable
325
+ expect(Widget.paper_trail.enabled?).to eq(false)
326
+ Widget.paper_trail.enable
327
+ expect(Widget.paper_trail.enabled?).to eq(true)
354
328
  end
355
329
  end
356
330
  end