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 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
- Unfortunately PaperTrail doesn't do this for `:has_many` associations (I can't get it to work) or `:belongs_to` (I ran out of time looking at `:has_many`).
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)
@@ -2,7 +2,20 @@ class Version < ActiveRecord::Base
2
2
  belongs_to :item, :polymorphic => true
3
3
  validates_presence_of :event
4
4
 
5
- def reify
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
- # Restore the model's has_one associations as they were when this version was
40
- # superseded by the next (because that's what the user was looking at when they
41
- # made the change).
42
- reify_has_ones model
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
- def reify_has_ones(model)
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? :version_until
79
- if (version_until = child.version_until(id))
80
- version_until.attributes.each do |k,v|
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
@@ -1,3 +1,3 @@
1
1
  module PaperTrail
2
- VERSION = '1.6.0'
2
+ VERSION = '1.6.1'
3
3
  end
@@ -46,8 +46,7 @@ end
46
46
 
47
47
  class HasPaperTrailModelTest < Test::Unit::TestCase
48
48
  load_schema
49
- =begin
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: 15
4
+ hash: 13
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 6
9
- - 0
10
- version: 1.6.0
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-20 00:00:00 +01:00
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
- version_requirements: &id001 !ruby/object:Gem::Requirement
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
- requirement: *id001
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
- version_requirements: &id002 !ruby/object:Gem::Requirement
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
- requirement: *id002
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
- version_requirements: &id003 !ruby/object:Gem::Requirement
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
- requirement: *id003
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
- version_requirements: &id004 !ruby/object:Gem::Requirement
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
- requirement: *id004
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
- version_requirements: &id005 !ruby/object:Gem::Requirement
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
- requirement: *id005
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
- version_requirements: &id006 !ruby/object:Gem::Requirement
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
- requirement: *id006
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
- version_requirements: &id007 !ruby/object:Gem::Requirement
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
- requirement: *id007
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: []