paper_trail 6.0.2 → 7.0.0

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