paper_trail 5.1.1 → 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CONTRIBUTING.md +9 -7
- data/.rubocop.yml +0 -4
- data/.rubocop_todo.yml +6 -1
- data/CHANGELOG.md +41 -1
- data/README.md +45 -43
- data/lib/paper_trail.rb +5 -0
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +1 -1
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +1 -1
- data/lib/paper_trail/frameworks/rails/controller.rb +7 -2
- data/lib/paper_trail/has_paper_trail.rb +196 -495
- data/lib/paper_trail/model_config.rb +195 -0
- data/lib/paper_trail/record_trail.rb +450 -0
- data/lib/paper_trail/reifier.rb +11 -11
- data/lib/paper_trail/version_number.rb +2 -2
- data/spec/models/boolit_spec.rb +2 -2
- data/spec/models/fluxor_spec.rb +4 -6
- data/spec/models/gadget_spec.rb +5 -7
- data/spec/models/not_on_update_spec.rb +2 -2
- data/spec/models/post_with_status_spec.rb +1 -1
- data/spec/models/widget_spec.rb +36 -66
- data/test/dummy/app/models/callback_modifier.rb +5 -5
- data/test/dummy/app/models/elephant.rb +1 -1
- data/test/functional/thread_safety_test.rb +4 -4
- data/test/unit/cleaner_test.rb +1 -1
- data/test/unit/model_test.rb +58 -48
- data/test/unit/protected_attrs_test.rb +4 -3
- data/test/unit/serializer_test.rb +4 -3
- metadata +4 -2
data/lib/paper_trail/reifier.rb
CHANGED
@@ -65,7 +65,7 @@ module PaperTrail
|
|
65
65
|
# @api private
|
66
66
|
def each_enabled_association(associations)
|
67
67
|
associations.each do |assoc|
|
68
|
-
next unless assoc.klass.
|
68
|
+
next unless assoc.klass.paper_trail.enabled?
|
69
69
|
yield assoc
|
70
70
|
end
|
71
71
|
end
|
@@ -112,7 +112,7 @@ module PaperTrail
|
|
112
112
|
collection_keys = through_collection.map { |through_model|
|
113
113
|
through_model.send(assoc.source_reflection.foreign_key)
|
114
114
|
}
|
115
|
-
version_id_subquery = assoc.klass.
|
115
|
+
version_id_subquery = assoc.klass.paper_trail.version_class.
|
116
116
|
select("MIN(id)").
|
117
117
|
where("item_type = ?", assoc.class_name).
|
118
118
|
where("item_id IN (?)", collection_keys).
|
@@ -140,7 +140,7 @@ module PaperTrail
|
|
140
140
|
# from the point in time identified by `transaction_id` or `version_at`.
|
141
141
|
# @api private
|
142
142
|
def load_version_for_habtm(assoc, id, transaction_id, version_at)
|
143
|
-
assoc.klass.
|
143
|
+
assoc.klass.paper_trail.version_class.
|
144
144
|
where("item_type = ?", assoc.klass.name).
|
145
145
|
where("item_id = ?", id).
|
146
146
|
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
@@ -153,8 +153,8 @@ module PaperTrail
|
|
153
153
|
# record from the point in time identified by `transaction_id` or `version_at`.
|
154
154
|
# @api private
|
155
155
|
def load_version_for_has_one(assoc, model, transaction_id, version_at)
|
156
|
-
version_table_name = model.class.
|
157
|
-
model.class.
|
156
|
+
version_table_name = model.class.paper_trail.version_class.table_name
|
157
|
+
model.class.paper_trail.version_class.joins(:version_associations).
|
158
158
|
where("version_associations.foreign_key_name = ?", assoc.foreign_key).
|
159
159
|
where("version_associations.foreign_key_id = ?", model.id).
|
160
160
|
where("#{version_table_name}.item_type = ?", assoc.class_name).
|
@@ -263,7 +263,7 @@ module PaperTrail
|
|
263
263
|
if options[:mark_for_destruction]
|
264
264
|
model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
|
265
265
|
else
|
266
|
-
model.appear_as_new_record do
|
266
|
+
model.paper_trail.appear_as_new_record do
|
267
267
|
model.send "#{assoc.name}=", nil
|
268
268
|
end
|
269
269
|
end
|
@@ -276,7 +276,7 @@ module PaperTrail
|
|
276
276
|
has_and_belongs_to_many: false
|
277
277
|
)
|
278
278
|
)
|
279
|
-
model.appear_as_new_record do
|
279
|
+
model.paper_trail.appear_as_new_record do
|
280
280
|
without_persisting(child) do
|
281
281
|
model.send "#{assoc.name}=", child
|
282
282
|
end
|
@@ -289,7 +289,7 @@ module PaperTrail
|
|
289
289
|
associations = model.class.reflect_on_all_associations(:belongs_to)
|
290
290
|
each_enabled_association(associations) do |assoc|
|
291
291
|
collection_key = model.send(assoc.association_foreign_key)
|
292
|
-
version = assoc.klass.
|
292
|
+
version = assoc.klass.paper_trail.version_class.
|
293
293
|
where("item_type = ?", assoc.class_name).
|
294
294
|
where("item_id = ?", collection_key).
|
295
295
|
where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
|
@@ -326,7 +326,7 @@ module PaperTrail
|
|
326
326
|
# Restore the `model`'s has_many associations not associated through
|
327
327
|
# another association.
|
328
328
|
def reify_has_many_directly(transaction_id, associations, model, options = {})
|
329
|
-
version_table_name = model.class.
|
329
|
+
version_table_name = model.class.paper_trail.version_class.table_name
|
330
330
|
each_enabled_association(associations) do |assoc|
|
331
331
|
version_id_subquery = PaperTrail::VersionAssociation.
|
332
332
|
joins(model.class.version_association_name).
|
@@ -366,7 +366,7 @@ module PaperTrail
|
|
366
366
|
|
367
367
|
def reify_has_and_belongs_to_many(transaction_id, model, options = {})
|
368
368
|
model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
|
369
|
-
papertrail_enabled = assoc.klass.
|
369
|
+
papertrail_enabled = assoc.klass.paper_trail.enabled?
|
370
370
|
next unless
|
371
371
|
model.class.paper_trail_save_join_tables.include?(assoc.name) ||
|
372
372
|
papertrail_enabled
|
@@ -429,7 +429,7 @@ module PaperTrail
|
|
429
429
|
#
|
430
430
|
def versions_by_id(klass, version_id_subquery)
|
431
431
|
klass.
|
432
|
-
|
432
|
+
paper_trail.version_class.
|
433
433
|
where("id IN (#{version_id_subquery})").
|
434
434
|
inject({}) { |a, e| a.merge!(e.item_id => e) }
|
435
435
|
end
|
data/spec/models/boolit_spec.rb
CHANGED
@@ -28,7 +28,7 @@ describe Boolit, type: :model do
|
|
28
28
|
end
|
29
29
|
|
30
30
|
it "should still be able to be reified and persisted" do
|
31
|
-
expect { subject.previous_version.save! }.to_not raise_error
|
31
|
+
expect { subject.paper_trail.previous_version.save! }.to_not raise_error
|
32
32
|
end
|
33
33
|
|
34
34
|
context "with `nil` attributes on the live instance" do
|
@@ -40,7 +40,7 @@ describe Boolit, type: :model do
|
|
40
40
|
after { PaperTrail.serializer = PaperTrail::Serializers::YAML }
|
41
41
|
|
42
42
|
it "should not overwrite that attribute during the reification process" do
|
43
|
-
expect(subject.previous_version.name).to be_nil
|
43
|
+
expect(subject.paper_trail.previous_version.name).to be_nil
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
data/spec/models/fluxor_spec.rb
CHANGED
@@ -7,12 +7,10 @@ describe Fluxor, type: :model do
|
|
7
7
|
|
8
8
|
describe "Methods" do
|
9
9
|
describe "Class" do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
it { expect(subject.paper_trail_enabled_for_model?).to be false }
|
10
|
+
describe ".paper_trail.enabled?" do
|
11
|
+
it "returns false" do
|
12
|
+
expect(Fluxor.paper_trail.enabled?).to eq(false)
|
13
|
+
end
|
16
14
|
end
|
17
15
|
end
|
18
16
|
end
|
data/spec/models/gadget_spec.rb
CHANGED
@@ -28,10 +28,8 @@ describe Gadget, type: :model do
|
|
28
28
|
describe '#changed_notably?' do
|
29
29
|
subject { Gadget.new(created_at: Time.now) }
|
30
30
|
|
31
|
-
it { expect(subject.private_methods).to include(:changed_notably?) }
|
32
|
-
|
33
31
|
context "create events" do
|
34
|
-
it { expect(subject.
|
32
|
+
it { expect(subject.paper_trail.changed_notably?).to be true }
|
35
33
|
end
|
36
34
|
|
37
35
|
context "update events" do
|
@@ -40,12 +38,12 @@ describe Gadget, type: :model do
|
|
40
38
|
context "without update timestamps" do
|
41
39
|
it "should only acknowledge non-ignored attrs" do
|
42
40
|
subject.name = "Wrench"
|
43
|
-
expect(subject.
|
41
|
+
expect(subject.paper_trail.changed_notably?).to be true
|
44
42
|
end
|
45
43
|
|
46
44
|
it "should not acknowledge ignored attr (brand)" do
|
47
45
|
subject.brand = "Acme"
|
48
|
-
expect(subject.
|
46
|
+
expect(subject.paper_trail.changed_notably?).to be false
|
49
47
|
end
|
50
48
|
end
|
51
49
|
|
@@ -53,13 +51,13 @@ describe Gadget, type: :model do
|
|
53
51
|
it "should only acknowledge non-ignored attrs" do
|
54
52
|
subject.name = "Wrench"
|
55
53
|
subject.updated_at = Time.now
|
56
|
-
expect(subject.
|
54
|
+
expect(subject.paper_trail.changed_notably?).to be true
|
57
55
|
end
|
58
56
|
|
59
57
|
it "should not acknowledge ignored attrs and timestamps only" do
|
60
58
|
subject.brand = "Acme"
|
61
59
|
subject.updated_at = Time.now
|
62
|
-
expect(subject.
|
60
|
+
expect(subject.paper_trail.changed_notably?).to be false
|
63
61
|
end
|
64
62
|
end
|
65
63
|
end
|
@@ -5,7 +5,7 @@ describe NotOnUpdate, type: :model do
|
|
5
5
|
let!(:record) { described_class.create! }
|
6
6
|
|
7
7
|
it "should create a version, regardless" do
|
8
|
-
expect { record.touch_with_version }.to change {
|
8
|
+
expect { record.paper_trail.touch_with_version }.to change {
|
9
9
|
PaperTrail::Version.count
|
10
10
|
}.by(+1)
|
11
11
|
end
|
@@ -14,7 +14,7 @@ describe NotOnUpdate, type: :model do
|
|
14
14
|
before = record.updated_at
|
15
15
|
# Travel 1 second because MySQL lacks sub-second resolution
|
16
16
|
Timecop.travel(1) do
|
17
|
-
record.touch_with_version
|
17
|
+
record.paper_trail.touch_with_version
|
18
18
|
end
|
19
19
|
expect(record.updated_at).to be > before
|
20
20
|
end
|
@@ -10,7 +10,7 @@ describe PostWithStatus, type: :model do
|
|
10
10
|
it "should stash the enum value properly in versions" do
|
11
11
|
post.published!
|
12
12
|
post.archived!
|
13
|
-
expect(post.previous_version.published?).to be true
|
13
|
+
expect(post.paper_trail.previous_version.published?).to be true
|
14
14
|
end
|
15
15
|
|
16
16
|
context "storing enum object_changes" do
|
data/spec/models/widget_spec.rb
CHANGED
@@ -64,11 +64,11 @@ describe Widget, type: :model do
|
|
64
64
|
|
65
65
|
subject { widget.versions.last.reify }
|
66
66
|
|
67
|
-
it { expect(subject).not_to be_live }
|
67
|
+
it { expect(subject.paper_trail).not_to be_live }
|
68
68
|
|
69
69
|
it "should clear the `versions_association_name` virtual attribute" do
|
70
70
|
subject.save!
|
71
|
-
expect(subject).to be_live
|
71
|
+
expect(subject.paper_trail).to be_live
|
72
72
|
end
|
73
73
|
|
74
74
|
it "corresponding version should use the widget updated_at" do
|
@@ -140,21 +140,21 @@ describe Widget, type: :model do
|
|
140
140
|
|
141
141
|
describe "Methods" do
|
142
142
|
describe "Instance", versioning: true do
|
143
|
-
describe '#
|
144
|
-
it { is_expected.to respond_to(:paper_trail_originator) }
|
145
|
-
|
143
|
+
describe '#paper_trail.originator' do
|
146
144
|
describe "return value" do
|
147
145
|
let(:orig_name) { FFaker::Name.name }
|
148
146
|
let(:new_name) { FFaker::Name.name }
|
149
147
|
before { PaperTrail.whodunnit = orig_name }
|
150
148
|
|
151
149
|
context "accessed from live model instance" do
|
152
|
-
specify { expect(widget).to be_live }
|
150
|
+
specify { expect(widget.paper_trail).to be_live }
|
153
151
|
|
154
152
|
it "should return the originator for the model at a given state" do
|
155
|
-
expect(widget.
|
156
|
-
widget.whodunnit(new_name) { |w|
|
157
|
-
|
153
|
+
expect(widget.paper_trail.originator).to eq(orig_name)
|
154
|
+
widget.paper_trail.whodunnit(new_name) { |w|
|
155
|
+
w.update_attributes(name: "Elizabeth")
|
156
|
+
}
|
157
|
+
expect(widget.paper_trail.originator).to eq(new_name)
|
158
158
|
end
|
159
159
|
end
|
160
160
|
|
@@ -169,7 +169,7 @@ describe Widget, type: :model do
|
|
169
169
|
let(:reified_widget) { widget.versions[1].reify }
|
170
170
|
|
171
171
|
it "should return the appropriate originator" do
|
172
|
-
expect(reified_widget.
|
172
|
+
expect(reified_widget.paper_trail.originator).to eq(orig_name)
|
173
173
|
end
|
174
174
|
|
175
175
|
it "should not create a new model instance" do
|
@@ -181,7 +181,7 @@ describe Widget, type: :model do
|
|
181
181
|
let(:reified_widget) { widget.versions[1].reify(dup: true) }
|
182
182
|
|
183
183
|
it "should return the appropriate originator" do
|
184
|
-
expect(reified_widget.
|
184
|
+
expect(reified_widget.paper_trail.originator).to eq(orig_name)
|
185
185
|
end
|
186
186
|
|
187
187
|
it "should not create a new model instance" do
|
@@ -192,35 +192,12 @@ describe Widget, type: :model do
|
|
192
192
|
end
|
193
193
|
end
|
194
194
|
|
195
|
-
describe "#originator" do
|
196
|
-
subject { widget }
|
197
|
-
|
198
|
-
it { is_expected.to respond_to(:originator) }
|
199
|
-
|
200
|
-
it "should set the invoke `paper_trail_originator`" do
|
201
|
-
allow(::ActiveSupport::Deprecation).to receive(:warn)
|
202
|
-
is_expected.to receive(:paper_trail_originator)
|
203
|
-
subject.originator
|
204
|
-
end
|
205
|
-
|
206
|
-
it "should display a deprecation warning" do
|
207
|
-
expect(::ActiveSupport::Deprecation).to receive(:warn).
|
208
|
-
with(/Use paper_trail_originator instead of originator/)
|
209
|
-
subject.originator
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
195
|
describe '#version_at' do
|
214
|
-
it { is_expected.to respond_to(:version_at) }
|
215
|
-
|
216
196
|
context "Timestamp argument is AFTER object has been destroyed" do
|
217
|
-
|
197
|
+
it "should return `nil`" do
|
218
198
|
widget.update_attribute(:name, "foobar")
|
219
199
|
widget.destroy
|
220
|
-
|
221
|
-
|
222
|
-
it "should return `nil`" do
|
223
|
-
expect(widget.version_at(Time.now)).to be_nil
|
200
|
+
expect(widget.paper_trail.version_at(Time.now)).to be_nil
|
224
201
|
end
|
225
202
|
end
|
226
203
|
end
|
@@ -231,7 +208,7 @@ describe Widget, type: :model do
|
|
231
208
|
context "no block given" do
|
232
209
|
it "should raise an error" do
|
233
210
|
expect {
|
234
|
-
widget.whodunnit("Ben")
|
211
|
+
widget.paper_trail.whodunnit("Ben")
|
235
212
|
}.to raise_error(ArgumentError, "expected to receive a block")
|
236
213
|
end
|
237
214
|
end
|
@@ -246,7 +223,7 @@ describe Widget, type: :model do
|
|
246
223
|
end
|
247
224
|
|
248
225
|
it "should modify value of `PaperTrail.whodunnit` while executing the block" do
|
249
|
-
widget.whodunnit(new_name) do
|
226
|
+
widget.paper_trail.whodunnit(new_name) do
|
250
227
|
expect(PaperTrail.whodunnit).to eq(new_name)
|
251
228
|
widget.update_attributes(name: "Elizabeth")
|
252
229
|
end
|
@@ -255,7 +232,9 @@ describe Widget, type: :model do
|
|
255
232
|
|
256
233
|
context "after executing the block" do
|
257
234
|
it "reverts value of whodunnit to previous value" do
|
258
|
-
widget.whodunnit(new_name) { |w|
|
235
|
+
widget.paper_trail.whodunnit(new_name) { |w|
|
236
|
+
w.update_attributes(name: "Elizabeth")
|
237
|
+
}
|
259
238
|
expect(PaperTrail.whodunnit).to eq(orig_name)
|
260
239
|
end
|
261
240
|
end
|
@@ -263,7 +242,7 @@ describe Widget, type: :model do
|
|
263
242
|
context "error within block" do
|
264
243
|
it "still reverts the whodunnit value to previous value" do
|
265
244
|
expect {
|
266
|
-
widget.whodunnit(new_name) { raise }
|
245
|
+
widget.paper_trail.whodunnit(new_name) { raise }
|
267
246
|
}.to raise_error(RuntimeError)
|
268
247
|
expect(PaperTrail.whodunnit).to eq(orig_name)
|
269
248
|
end
|
@@ -272,13 +251,11 @@ describe Widget, type: :model do
|
|
272
251
|
end
|
273
252
|
|
274
253
|
describe '#touch_with_version' do
|
275
|
-
it { is_expected.to respond_to(:touch_with_version) }
|
276
|
-
|
277
254
|
it "creates a version" do
|
278
255
|
count = widget.versions.size
|
279
256
|
# Travel 1 second because MySQL lacks sub-second resolution
|
280
257
|
Timecop.travel(1) do
|
281
|
-
widget.touch_with_version
|
258
|
+
widget.paper_trail.touch_with_version
|
282
259
|
end
|
283
260
|
expect(widget.versions.size).to eq(count + 1)
|
284
261
|
end
|
@@ -287,7 +264,7 @@ describe Widget, type: :model do
|
|
287
264
|
time_was = widget.updated_at
|
288
265
|
# Travel 1 second because MySQL lacks sub-second resolution
|
289
266
|
Timecop.travel(1) do
|
290
|
-
widget.touch_with_version
|
267
|
+
widget.paper_trail.touch_with_version
|
291
268
|
end
|
292
269
|
expect(widget.updated_at).to be > time_was
|
293
270
|
end
|
@@ -295,33 +272,26 @@ describe Widget, type: :model do
|
|
295
272
|
end
|
296
273
|
|
297
274
|
describe "Class" do
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
it { expect(subject.paper_trail_enabled_for_model?).to be true }
|
275
|
+
describe ".paper_trail.enabled?" do
|
276
|
+
it "returns true" do
|
277
|
+
expect(Widget.paper_trail.enabled?).to eq(true)
|
278
|
+
end
|
304
279
|
end
|
305
280
|
|
306
|
-
describe
|
307
|
-
it
|
308
|
-
|
309
|
-
|
310
|
-
expect(
|
311
|
-
subject.paper_trail_off!
|
312
|
-
expect(subject.paper_trail_enabled_for_model?).to be false
|
281
|
+
describe ".disable" do
|
282
|
+
it "should set the `paper_trail.enabled?` to `false`" do
|
283
|
+
expect(Widget.paper_trail.enabled?).to eq(true)
|
284
|
+
Widget.paper_trail.disable
|
285
|
+
expect(Widget.paper_trail.enabled?).to eq(false)
|
313
286
|
end
|
314
287
|
end
|
315
288
|
|
316
|
-
describe
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
expect(subject.paper_trail_enabled_for_model?).to be false
|
323
|
-
subject.paper_trail_on!
|
324
|
-
expect(subject.paper_trail_enabled_for_model?).to be true
|
289
|
+
describe ".enable" do
|
290
|
+
it "should set the `paper_trail.enabled?` to `true`" do
|
291
|
+
Widget.paper_trail.disable
|
292
|
+
expect(Widget.paper_trail.enabled?).to eq(false)
|
293
|
+
Widget.paper_trail.enable
|
294
|
+
expect(Widget.paper_trail.enabled?).to eq(true)
|
325
295
|
end
|
326
296
|
end
|
327
297
|
end
|
@@ -17,27 +17,27 @@ end
|
|
17
17
|
|
18
18
|
class BeforeDestroyModifier < CallbackModifier
|
19
19
|
has_paper_trail on: []
|
20
|
-
|
20
|
+
paper_trail.on_destroy :before
|
21
21
|
end
|
22
22
|
|
23
23
|
class AfterDestroyModifier < CallbackModifier
|
24
24
|
has_paper_trail on: []
|
25
|
-
|
25
|
+
paper_trail.on_destroy :after
|
26
26
|
end
|
27
27
|
|
28
28
|
class NoArgDestroyModifier < CallbackModifier
|
29
29
|
has_paper_trail on: []
|
30
|
-
|
30
|
+
paper_trail.on_destroy
|
31
31
|
end
|
32
32
|
|
33
33
|
class UpdateModifier < CallbackModifier
|
34
34
|
has_paper_trail on: []
|
35
|
-
|
35
|
+
paper_trail.on_update
|
36
36
|
end
|
37
37
|
|
38
38
|
class CreateModifier < CallbackModifier
|
39
39
|
has_paper_trail on: []
|
40
|
-
|
40
|
+
paper_trail.on_create
|
41
41
|
end
|
42
42
|
|
43
43
|
class DefaultModifier < CallbackModifier
|