paper_trail 1.6.0 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +21 -3
- data/lib/paper_trail/has_paper_trail.rb +6 -11
- data/lib/paper_trail/version.rb +33 -9
- data/lib/paper_trail/version_number.rb +1 -1
- data/test/paper_trail_model_test.rb +52 -6
- metadata +25 -25
data/README.md
CHANGED
@@ -171,6 +171,13 @@ Finally, if you got an item by reifying one of its versions, you can navigate ba
|
|
171
171
|
>> widget = latest_version.reify
|
172
172
|
>> widget.version == latest_version # true
|
173
173
|
|
174
|
+
You can find out whether a model instance is the current, live one -- or whether it came instead from a previous version -- with `live?`:
|
175
|
+
|
176
|
+
>> widget = Widget.find 42
|
177
|
+
>> widget.live? # true
|
178
|
+
>> widget = widget.versions.last.reify
|
179
|
+
>> widget.live? # false
|
180
|
+
|
174
181
|
|
175
182
|
## Finding Out Who Was Responsible For A Change
|
176
183
|
|
@@ -215,7 +222,7 @@ To find out who made a `version`'s object look that way, use `version.originator
|
|
215
222
|
|
216
223
|
## Has-One Associations
|
217
224
|
|
218
|
-
PaperTrail automatically restores `:has_one` associations as they were at the time.
|
225
|
+
PaperTrail automatically restores `:has_one` associations as they were at (actually, 3 seconds before) the time.
|
219
226
|
|
220
227
|
class Treasure < ActiveRecord::Base
|
221
228
|
has_one :location
|
@@ -230,8 +237,19 @@ PaperTrail automatically restores `:has_one` associations as they were at the ti
|
|
230
237
|
>> t = treasure.versions.last.reify
|
231
238
|
>> t.amount # 100
|
232
239
|
>> t.location.latitude # 12.345
|
233
|
-
|
234
|
-
|
240
|
+
|
241
|
+
The implementation is complicated by the edge case where the parent and child are updated in one go, e.g. in one web request or database transaction. PaperTrail doesn't know about different models being updated "together", so you can't ask it definitively to get the child as it was before the joint parent-and-child update.
|
242
|
+
|
243
|
+
The correct solution is to make PaperTrail aware of requests or transactions (c.f. [Efficiency's transaction ID middleware](http://github.com/efficiency20/ops_middleware/blob/master/lib/e20/ops/middleware/transaction_id_middleware.rb)). In the meantime we work around the problem by finding the child as it was a few seconds before the parent was updated. By default we go 3 seconds before but you can change this by passing the `:has_one` option to `reify`:
|
244
|
+
|
245
|
+
>> t = treasure.versions.last.reify(:has_one => 1) # look back 1 second instead of 3
|
246
|
+
|
247
|
+
If you are shuddering, take solace from knowing you can opt out of these shenanigans:
|
248
|
+
|
249
|
+
>> t = treasure.versions.last.reify(:has_one => false) # I say no to "workarounds"!
|
250
|
+
|
251
|
+
Opting out means your `:has_one` associated objects will be the live ones, not the ones the user saw at the time. Since PaperTrail doesn't auto-restore `:has_many` associations (I can't get it to work) or `:belongs_to` (I ran out of time looking at `:has_many`), this at least makes your associations wrong consistently ;)
|
252
|
+
|
235
253
|
|
236
254
|
|
237
255
|
## Has-Many-Through Associations
|
@@ -78,6 +78,12 @@ module PaperTrail
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
+
# Returns true if this instance is the current, live one;
|
82
|
+
# returns false if this instance came from a previous version.
|
83
|
+
def live?
|
84
|
+
version.nil?
|
85
|
+
end
|
86
|
+
|
81
87
|
# Returns who put the object into its current state.
|
82
88
|
def originator
|
83
89
|
versions.last.try :whodunnit
|
@@ -105,17 +111,6 @@ module PaperTrail
|
|
105
111
|
subsequent_version.reify if subsequent_version
|
106
112
|
end
|
107
113
|
|
108
|
-
protected
|
109
|
-
|
110
|
-
# Returns the object (not a Version) as it was until the version record
|
111
|
-
# with the given id.
|
112
|
-
def version_until(id)
|
113
|
-
# Because a version stores how its object looked *before* the change,
|
114
|
-
# we need to look for the first version created *on or after* the id.
|
115
|
-
version = versions.first :conditions => ['id >= ?', id], :order => 'id ASC'
|
116
|
-
version ? version.reify : self
|
117
|
-
end
|
118
|
-
|
119
114
|
private
|
120
115
|
|
121
116
|
def merge_metadata(data)
|
data/lib/paper_trail/version.rb
CHANGED
@@ -2,7 +2,20 @@ class Version < ActiveRecord::Base
|
|
2
2
|
belongs_to :item, :polymorphic => true
|
3
3
|
validates_presence_of :event
|
4
4
|
|
5
|
-
|
5
|
+
# Restore the item from this version.
|
6
|
+
#
|
7
|
+
# This will automatically restore all :has_one associations as they were "at the time",
|
8
|
+
# if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
|
9
|
+
# to work so you can either change the lookback period (from the default 3 seconds) or
|
10
|
+
# opt out.
|
11
|
+
#
|
12
|
+
# Options:
|
13
|
+
# +:has_one+ set to `false` to opt out of has_one reification.
|
14
|
+
# set to a float to change the lookback time (check whether your db supports
|
15
|
+
# sub-second datetimes if you want them).
|
16
|
+
def reify(options = {})
|
17
|
+
options.reverse_merge! :has_one => 3
|
18
|
+
|
6
19
|
unless object.nil?
|
7
20
|
attrs = YAML::load object
|
8
21
|
|
@@ -36,10 +49,11 @@ class Version < ActiveRecord::Base
|
|
36
49
|
end
|
37
50
|
|
38
51
|
model.version = self
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
52
|
+
|
53
|
+
unless options[:has_one] == false
|
54
|
+
reify_has_ones model, options[:has_one]
|
55
|
+
end
|
56
|
+
|
43
57
|
model
|
44
58
|
end
|
45
59
|
end
|
@@ -72,12 +86,22 @@ class Version < ActiveRecord::Base
|
|
72
86
|
|
73
87
|
private
|
74
88
|
|
75
|
-
|
89
|
+
# Restore the `model`'s has_one associations as they were when this version was
|
90
|
+
# superseded by the next (because that's what the user was looking at when they
|
91
|
+
# made the change).
|
92
|
+
#
|
93
|
+
# The `lookback` sets how many seconds before the model's change we go.
|
94
|
+
def reify_has_ones(model, lookback)
|
76
95
|
model.class.reflect_on_all_associations(:has_one).each do |assoc|
|
77
96
|
child = model.send assoc.name
|
78
|
-
if child.respond_to? :
|
79
|
-
|
80
|
-
|
97
|
+
if child.respond_to? :version_at
|
98
|
+
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
|
99
|
+
# Ideally we want the version of the child as it was just before the parent was updated...
|
100
|
+
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
|
101
|
+
# updated on the same form), it's impossible to tell when the overall update started;
|
102
|
+
# and therefore impossible to know when "just before" was.
|
103
|
+
if (child_as_it_was = child.version_at(created_at - lookback.seconds))
|
104
|
+
child_as_it_was.attributes.each do |k,v|
|
81
105
|
model.send(assoc.name).send "#{k}=", v rescue nil
|
82
106
|
end
|
83
107
|
else
|
@@ -46,8 +46,7 @@ end
|
|
46
46
|
|
47
47
|
class HasPaperTrailModelTest < Test::Unit::TestCase
|
48
48
|
load_schema
|
49
|
-
|
50
|
-
=end
|
49
|
+
|
51
50
|
context 'A record' do
|
52
51
|
setup { @article = Article.create }
|
53
52
|
|
@@ -70,6 +69,10 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
70
69
|
assert_equal [], @widget.versions
|
71
70
|
end
|
72
71
|
|
72
|
+
should 'be live' do
|
73
|
+
assert @widget.live?
|
74
|
+
end
|
75
|
+
|
73
76
|
|
74
77
|
context 'which is then created' do
|
75
78
|
setup { @widget.update_attributes :name => 'Henry' }
|
@@ -87,6 +90,10 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
87
90
|
assert_match /create/i, @widget.versions.first.event
|
88
91
|
end
|
89
92
|
|
93
|
+
should 'be live' do
|
94
|
+
assert @widget.live?
|
95
|
+
end
|
96
|
+
|
90
97
|
|
91
98
|
context 'and then updated without any changes' do
|
92
99
|
setup { @widget.save }
|
@@ -120,6 +127,10 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
120
127
|
assert_match /update/i, @widget.versions.last.event
|
121
128
|
end
|
122
129
|
|
130
|
+
should 'have versions that are not live' do
|
131
|
+
assert @widget.versions.map(&:reify).compact.all? { |w| !w.live? }
|
132
|
+
end
|
133
|
+
|
123
134
|
|
124
135
|
context 'and has one associated object' do
|
125
136
|
setup do
|
@@ -650,13 +661,16 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
650
661
|
|
651
662
|
context 'A model with a has_one association' do
|
652
663
|
setup { @widget = Widget.create :name => 'widget_0' }
|
664
|
+
|
653
665
|
context 'before the associated was created' do
|
654
666
|
setup do
|
655
667
|
@widget.update_attributes :name => 'widget_1'
|
656
668
|
@wotsit = @widget.create_wotsit :name => 'wotsit_0'
|
657
669
|
end
|
670
|
+
|
658
671
|
context 'when reified' do
|
659
|
-
setup { @widget_0 = @widget.versions.last.reify }
|
672
|
+
setup { @widget_0 = @widget.versions.last.reify(:has_one => 1) }
|
673
|
+
|
660
674
|
should 'see the associated as it was at the time' do
|
661
675
|
assert_nil @widget_0.wotsit
|
662
676
|
end
|
@@ -666,10 +680,14 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
666
680
|
context 'where the associated is created between model versions' do
|
667
681
|
setup do
|
668
682
|
@wotsit = @widget.create_wotsit :name => 'wotsit_0'
|
683
|
+
make_last_version_earlier @wotsit
|
684
|
+
|
669
685
|
@widget.update_attributes :name => 'widget_1'
|
670
686
|
end
|
687
|
+
|
671
688
|
context 'when reified' do
|
672
|
-
setup { @widget_0 = @widget.versions.last.reify }
|
689
|
+
setup { @widget_0 = @widget.versions.last.reify(:has_one => 1) }
|
690
|
+
|
673
691
|
should 'see the associated as it was at the time' do
|
674
692
|
assert_equal 'wotsit_0', @widget_0.wotsit.name
|
675
693
|
end
|
@@ -678,24 +696,42 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
678
696
|
context 'and then the associated is updated between model versions' do
|
679
697
|
setup do
|
680
698
|
@wotsit.update_attributes :name => 'wotsit_1'
|
699
|
+
make_last_version_earlier @wotsit
|
681
700
|
@wotsit.update_attributes :name => 'wotsit_2'
|
701
|
+
make_last_version_earlier @wotsit
|
702
|
+
|
682
703
|
@widget.update_attributes :name => 'widget_2'
|
704
|
+
@wotsit.update_attributes :name => 'wotsit_3'
|
683
705
|
end
|
706
|
+
|
684
707
|
context 'when reified' do
|
685
|
-
setup { @widget_1 = @widget.versions.last.reify }
|
708
|
+
setup { @widget_1 = @widget.versions.last.reify(:has_one => 1) }
|
709
|
+
|
686
710
|
should 'see the associated as it was at the time' do
|
687
711
|
assert_equal 'wotsit_2', @widget_1.wotsit.name
|
688
712
|
end
|
689
713
|
end
|
714
|
+
|
715
|
+
context 'when reified opting out of has_one reification' do
|
716
|
+
setup { @widget_1 = @widget.versions.last.reify(:has_one => false) }
|
717
|
+
|
718
|
+
should 'see the associated as it is live' do
|
719
|
+
assert_equal 'wotsit_3', @widget_1.wotsit.name
|
720
|
+
end
|
721
|
+
end
|
690
722
|
end
|
691
723
|
|
692
724
|
context 'and then the associated is destroyed between model versions' do
|
693
725
|
setup do
|
694
726
|
@wotsit.destroy
|
727
|
+
make_last_version_earlier @wotsit
|
728
|
+
|
695
729
|
@widget.update_attributes :name => 'widget_3'
|
696
730
|
end
|
731
|
+
|
697
732
|
context 'when reified' do
|
698
|
-
setup { @widget_2 = @widget.versions.last.reify }
|
733
|
+
setup { @widget_2 = @widget.versions.last.reify(:has_one => 1) }
|
734
|
+
|
699
735
|
should 'see the associated as it was at the time' do
|
700
736
|
assert_nil @widget_2.wotsit
|
701
737
|
end
|
@@ -704,4 +740,14 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
|
|
704
740
|
end
|
705
741
|
end
|
706
742
|
|
743
|
+
private
|
744
|
+
|
745
|
+
# Updates `model`'s last version so it looks like the version was
|
746
|
+
# created 2 seconds ago.
|
747
|
+
def make_last_version_earlier(model)
|
748
|
+
Version.record_timestamps = false
|
749
|
+
model.versions.last.update_attributes :created_at => 2.seconds.ago
|
750
|
+
Version.record_timestamps = true
|
751
|
+
end
|
752
|
+
|
707
753
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paper_trail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 6
|
9
|
-
-
|
10
|
-
version: 1.6.
|
9
|
+
- 1
|
10
|
+
version: 1.6.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Andy Stewart
|
@@ -15,14 +15,13 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-10-
|
18
|
+
date: 2010-10-21 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: bundler
|
23
|
-
type: :development
|
24
23
|
prerelease: false
|
25
|
-
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
25
|
none: false
|
27
26
|
requirements:
|
28
27
|
- - ~>
|
@@ -32,12 +31,12 @@ dependencies:
|
|
32
31
|
- 1
|
33
32
|
- 0
|
34
33
|
version: "1.0"
|
35
|
-
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake
|
38
|
-
type: :development
|
39
38
|
prerelease: false
|
40
|
-
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
40
|
none: false
|
42
41
|
requirements:
|
43
42
|
- - "="
|
@@ -48,12 +47,12 @@ dependencies:
|
|
48
47
|
- 8
|
49
48
|
- 7
|
50
49
|
version: 0.8.7
|
51
|
-
|
50
|
+
type: :development
|
51
|
+
version_requirements: *id002
|
52
52
|
- !ruby/object:Gem::Dependency
|
53
53
|
name: shoulda
|
54
|
-
type: :development
|
55
54
|
prerelease: false
|
56
|
-
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
56
|
none: false
|
58
57
|
requirements:
|
59
58
|
- - "="
|
@@ -64,12 +63,12 @@ dependencies:
|
|
64
63
|
- 10
|
65
64
|
- 3
|
66
65
|
version: 2.10.3
|
67
|
-
|
66
|
+
type: :development
|
67
|
+
version_requirements: *id003
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
69
|
name: activesupport
|
70
|
-
type: :development
|
71
70
|
prerelease: false
|
72
|
-
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
72
|
none: false
|
74
73
|
requirements:
|
75
74
|
- - ~>
|
@@ -79,12 +78,12 @@ dependencies:
|
|
79
78
|
- 2
|
80
79
|
- 3
|
81
80
|
version: "2.3"
|
82
|
-
|
81
|
+
type: :development
|
82
|
+
version_requirements: *id004
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: sqlite3-ruby
|
85
|
-
type: :development
|
86
85
|
prerelease: false
|
87
|
-
|
86
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
88
87
|
none: false
|
89
88
|
requirements:
|
90
89
|
- - ~>
|
@@ -94,12 +93,12 @@ dependencies:
|
|
94
93
|
- 1
|
95
94
|
- 2
|
96
95
|
version: "1.2"
|
97
|
-
|
96
|
+
type: :development
|
97
|
+
version_requirements: *id005
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
99
|
name: activerecord
|
100
|
-
type: :runtime
|
101
100
|
prerelease: false
|
102
|
-
|
101
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
103
102
|
none: false
|
104
103
|
requirements:
|
105
104
|
- - ">="
|
@@ -109,12 +108,12 @@ dependencies:
|
|
109
108
|
- 2
|
110
109
|
- 3
|
111
110
|
version: "2.3"
|
112
|
-
|
111
|
+
type: :runtime
|
112
|
+
version_requirements: *id006
|
113
113
|
- !ruby/object:Gem::Dependency
|
114
114
|
name: actionpack
|
115
|
-
type: :runtime
|
116
115
|
prerelease: false
|
117
|
-
|
116
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
118
117
|
none: false
|
119
118
|
requirements:
|
120
119
|
- - ">="
|
@@ -124,7 +123,8 @@ dependencies:
|
|
124
123
|
- 2
|
125
124
|
- 3
|
126
125
|
version: "2.3"
|
127
|
-
|
126
|
+
type: :runtime
|
127
|
+
version_requirements: *id007
|
128
128
|
description: Track changes to your models' data. Good for auditing or versioning.
|
129
129
|
email: boss@airbladesoftware.com
|
130
130
|
executables: []
|