paper_trail_without_deprecated 3.0.0.beta1

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +21 -0
  5. data/CHANGELOG.md +68 -0
  6. data/Gemfile +2 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +979 -0
  9. data/Rakefile +18 -0
  10. data/gemfiles/3.0.gemfile +31 -0
  11. data/lib/generators/paper_trail/USAGE +2 -0
  12. data/lib/generators/paper_trail/install_generator.rb +23 -0
  13. data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
  14. data/lib/generators/paper_trail/templates/create_versions.rb +18 -0
  15. data/lib/paper_trail.rb +115 -0
  16. data/lib/paper_trail/cleaner.rb +34 -0
  17. data/lib/paper_trail/config.rb +14 -0
  18. data/lib/paper_trail/frameworks/cucumber.rb +31 -0
  19. data/lib/paper_trail/frameworks/rails.rb +79 -0
  20. data/lib/paper_trail/frameworks/rspec.rb +24 -0
  21. data/lib/paper_trail/frameworks/rspec/extensions.rb +20 -0
  22. data/lib/paper_trail/frameworks/sinatra.rb +31 -0
  23. data/lib/paper_trail/has_paper_trail.rb +308 -0
  24. data/lib/paper_trail/serializers/json.rb +17 -0
  25. data/lib/paper_trail/serializers/yaml.rb +17 -0
  26. data/lib/paper_trail/version.rb +200 -0
  27. data/lib/paper_trail/version_number.rb +3 -0
  28. data/paper_trail.gemspec +36 -0
  29. data/spec/models/widget_spec.rb +13 -0
  30. data/spec/paper_trail_spec.rb +47 -0
  31. data/spec/spec_helper.rb +41 -0
  32. data/test/custom_json_serializer.rb +13 -0
  33. data/test/dummy/Rakefile +7 -0
  34. data/test/dummy/app/controllers/application_controller.rb +17 -0
  35. data/test/dummy/app/controllers/test_controller.rb +5 -0
  36. data/test/dummy/app/controllers/widgets_controller.rb +31 -0
  37. data/test/dummy/app/helpers/application_helper.rb +2 -0
  38. data/test/dummy/app/models/animal.rb +4 -0
  39. data/test/dummy/app/models/article.rb +16 -0
  40. data/test/dummy/app/models/authorship.rb +5 -0
  41. data/test/dummy/app/models/book.rb +5 -0
  42. data/test/dummy/app/models/cat.rb +2 -0
  43. data/test/dummy/app/models/document.rb +4 -0
  44. data/test/dummy/app/models/dog.rb +2 -0
  45. data/test/dummy/app/models/elephant.rb +3 -0
  46. data/test/dummy/app/models/fluxor.rb +3 -0
  47. data/test/dummy/app/models/foo_widget.rb +2 -0
  48. data/test/dummy/app/models/legacy_widget.rb +4 -0
  49. data/test/dummy/app/models/person.rb +28 -0
  50. data/test/dummy/app/models/post.rb +4 -0
  51. data/test/dummy/app/models/protected_widget.rb +3 -0
  52. data/test/dummy/app/models/song.rb +12 -0
  53. data/test/dummy/app/models/translation.rb +4 -0
  54. data/test/dummy/app/models/widget.rb +10 -0
  55. data/test/dummy/app/models/wotsit.rb +4 -0
  56. data/test/dummy/app/versions/post_version.rb +3 -0
  57. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  58. data/test/dummy/config.ru +4 -0
  59. data/test/dummy/config/application.rb +63 -0
  60. data/test/dummy/config/boot.rb +10 -0
  61. data/test/dummy/config/database.yml +22 -0
  62. data/test/dummy/config/environment.rb +5 -0
  63. data/test/dummy/config/environments/development.rb +40 -0
  64. data/test/dummy/config/environments/production.rb +73 -0
  65. data/test/dummy/config/environments/test.rb +37 -0
  66. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/test/dummy/config/initializers/inflections.rb +10 -0
  68. data/test/dummy/config/initializers/mime_types.rb +5 -0
  69. data/test/dummy/config/initializers/paper_trail.rb +5 -0
  70. data/test/dummy/config/initializers/secret_token.rb +7 -0
  71. data/test/dummy/config/initializers/session_store.rb +8 -0
  72. data/test/dummy/config/locales/en.yml +5 -0
  73. data/test/dummy/config/routes.rb +3 -0
  74. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +136 -0
  75. data/test/dummy/db/schema.rb +101 -0
  76. data/test/dummy/public/404.html +26 -0
  77. data/test/dummy/public/422.html +26 -0
  78. data/test/dummy/public/500.html +26 -0
  79. data/test/dummy/public/favicon.ico +0 -0
  80. data/test/dummy/public/javascripts/application.js +2 -0
  81. data/test/dummy/public/javascripts/controls.js +965 -0
  82. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  83. data/test/dummy/public/javascripts/effects.js +1123 -0
  84. data/test/dummy/public/javascripts/prototype.js +6001 -0
  85. data/test/dummy/public/javascripts/rails.js +175 -0
  86. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  87. data/test/dummy/script/rails +6 -0
  88. data/test/functional/controller_test.rb +90 -0
  89. data/test/functional/modular_sinatra_test.rb +44 -0
  90. data/test/functional/sinatra_test.rb +45 -0
  91. data/test/functional/thread_safety_test.rb +26 -0
  92. data/test/paper_trail_test.rb +27 -0
  93. data/test/test_helper.rb +40 -0
  94. data/test/unit/cleaner_test.rb +143 -0
  95. data/test/unit/inheritance_column_test.rb +43 -0
  96. data/test/unit/model_test.rb +1314 -0
  97. data/test/unit/protected_attrs_test.rb +46 -0
  98. data/test/unit/serializer_test.rb +117 -0
  99. data/test/unit/serializers/json_test.rb +40 -0
  100. data/test/unit/serializers/mixin_json_test.rb +36 -0
  101. data/test/unit/serializers/mixin_yaml_test.rb +49 -0
  102. data/test/unit/serializers/yaml_test.rb +40 -0
  103. data/test/unit/timestamp_test.rb +44 -0
  104. data/test/unit/version_test.rb +74 -0
  105. metadata +286 -0
@@ -0,0 +1,308 @@
1
+ module PaperTrail
2
+ module Model
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ end
7
+
8
+
9
+ module ClassMethods
10
+ # Declare this in your model to track every create, update, and destroy. Each version of
11
+ # the model is available in the `versions` association.
12
+ #
13
+ # Options:
14
+ # :on the events to track (optional; defaults to all of them). Set to an array of
15
+ # `:create`, `:update`, `:destroy` as desired.
16
+ # :class_name the name of a custom Version class. This class should inherit from Version.
17
+ # :ignore an array of attributes for which a new `Version` will not be created if only they change.
18
+ # :if, :unless Procs that allow to specify conditions when to save versions for an object
19
+ # :only inverse of `ignore` - a new `Version` will be created only for these attributes if supplied
20
+ # :skip fields to ignore completely. As with `ignore`, updates to these fields will not create
21
+ # a new `Version`. In addition, these fields will not be included in the serialized versions
22
+ # of the object whenever a new `Version` is created.
23
+ # :meta a hash of extra data to store. You must add a column to the `versions` table for each key.
24
+ # Values are objects or procs (which are called with `self`, i.e. the model with the paper
25
+ # trail). See `PaperTrail::Controller.info_for_paper_trail` for how to store data from
26
+ # the controller.
27
+ # :versions the name to use for the versions association. Default is `:versions`.
28
+ # :version the name to use for the method which returns the version the instance was reified from.
29
+ # Default is `:version`.
30
+ def has_paper_trail(options = {})
31
+ # Lazily include the instance methods so we don't clutter up
32
+ # any more ActiveRecord models than we have to.
33
+ send :include, InstanceMethods
34
+
35
+ class_attribute :version_association_name
36
+ self.version_association_name = options[:version] || :version
37
+
38
+ # The version this instance was reified from.
39
+ attr_accessor self.version_association_name
40
+
41
+ class_attribute :version_class_name
42
+ self.version_class_name = options[:class_name] || 'PaperTrail::Version'
43
+
44
+ class_attribute :paper_trail_options
45
+ self.paper_trail_options = options.dup
46
+
47
+ [:ignore, :skip, :only].each do |k|
48
+ paper_trail_options[k] =
49
+ ([paper_trail_options[k]].flatten.compact || []).map &:to_s
50
+ end
51
+
52
+ paper_trail_options[:meta] ||= {}
53
+
54
+ class_attribute :paper_trail_enabled_for_model
55
+ self.paper_trail_enabled_for_model = true
56
+
57
+ class_attribute :versions_association_name
58
+ self.versions_association_name = options[:versions] || :versions
59
+
60
+ attr_accessor :paper_trail_event
61
+
62
+ if ActiveRecord::VERSION::STRING.to_f >= 4.0 # `has_many` syntax for specifying order uses a lambda in Rails 4
63
+ has_many self.versions_association_name,
64
+ lambda { order("#{PaperTrail.timestamp_field} ASC") },
65
+ :class_name => self.version_class_name, :as => :item
66
+ else
67
+ has_many self.versions_association_name,
68
+ :class_name => version_class_name,
69
+ :as => :item,
70
+ :order => "#{PaperTrail.timestamp_field} ASC"
71
+ end
72
+
73
+ options_on = Array(options[:on]) # so that a single symbol can be passed in without wrapping it in an `Array`
74
+ after_create :record_create, :if => :save_version? if options_on.empty? || options_on.include?(:create)
75
+ before_update :record_update, :if => :save_version? if options_on.empty? || options_on.include?(:update)
76
+ after_destroy :record_destroy, :if => :save_version? if options_on.empty? || options_on.include?(:destroy)
77
+ end
78
+
79
+ # Switches PaperTrail off for this class.
80
+ def paper_trail_off
81
+ self.paper_trail_enabled_for_model = false
82
+ end
83
+
84
+ # Switches PaperTrail on for this class.
85
+ def paper_trail_on
86
+ self.paper_trail_enabled_for_model = true
87
+ end
88
+
89
+ # Used for Version#object attribute
90
+ def serialize_attributes_for_paper_trail(attributes)
91
+ serialized_attributes.each do |key, coder|
92
+ if attributes.key?(key)
93
+ coder = PaperTrail::Serializers::Yaml unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
94
+ attributes[key] = coder.dump(attributes[key])
95
+ end
96
+ end
97
+ end
98
+
99
+ def unserialize_attributes_for_paper_trail(attributes)
100
+ serialized_attributes.each do |key, coder|
101
+ if attributes.key?(key)
102
+ coder = PaperTrail::Serializers::Yaml unless coder.respond_to?(:dump)
103
+ attributes[key] = coder.load(attributes[key])
104
+ end
105
+ end
106
+ end
107
+
108
+ # Used for Version#object_changes attribute
109
+ def serialize_attribute_changes(changes)
110
+ serialized_attributes.each do |key, coder|
111
+ if changes.key?(key)
112
+ coder = PaperTrail::Serializers::Yaml unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
113
+ old_value, new_value = changes[key]
114
+ changes[key] = [coder.dump(old_value),
115
+ coder.dump(new_value)]
116
+ end
117
+ end
118
+ end
119
+
120
+ def unserialize_attribute_changes(changes)
121
+ serialized_attributes.each do |key, coder|
122
+ if changes.key?(key)
123
+ coder = PaperTrail::Serializers::Yaml unless coder.respond_to?(:dump)
124
+ old_value, new_value = changes[key]
125
+ changes[key] = [coder.load(old_value),
126
+ coder.load(new_value)]
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Wrap the following methods in a module so we can include them only in the
133
+ # ActiveRecord models that declare `has_paper_trail`.
134
+ module InstanceMethods
135
+ # Returns true if this instance is the current, live one;
136
+ # returns false if this instance came from a previous version.
137
+ def live?
138
+ source_version.nil?
139
+ end
140
+
141
+ # Returns who put the object into its current state.
142
+ def originator
143
+ version_class.with_item_keys(self.class.base_class.name, id).last.try :whodunnit
144
+ end
145
+
146
+ # Returns the object (not a Version) as it was at the given timestamp.
147
+ def version_at(timestamp, reify_options={})
148
+ # Because a version stores how its object looked *before* the change,
149
+ # we need to look for the first version created *after* the timestamp.
150
+ v = send(self.class.versions_association_name).following(timestamp).first
151
+ v ? v.reify(reify_options) : self
152
+ end
153
+
154
+ # Returns the objects (not Versions) as they were between the given times.
155
+ def versions_between(start_time, end_time, reify_options={})
156
+ versions = send(self.class.versions_association_name).between(start_time, end_time)
157
+ versions.collect { |version| version_at(version.send PaperTrail.timestamp_field) }
158
+ end
159
+
160
+ # Returns the object (not a Version) as it was most recently.
161
+ def previous_version
162
+ preceding_version = source_version ? source_version.previous : send(self.class.versions_association_name).last
163
+ preceding_version.reify if preceding_version
164
+ end
165
+
166
+ # Returns the object (not a Version) as it became next.
167
+ # NOTE: if self (the item) was not reified from a version, i.e. it is the
168
+ # "live" item, we return nil. Perhaps we should return self instead?
169
+ def next_version
170
+ subsequent_version = source_version.next
171
+ subsequent_version ? subsequent_version.reify : self.class.find(self.id)
172
+ rescue
173
+ nil
174
+ end
175
+
176
+ # Executes the given method or block without creating a new version.
177
+ def without_versioning(method = nil)
178
+ paper_trail_was_enabled = self.paper_trail_enabled_for_model
179
+ self.class.paper_trail_off
180
+ method ? method.to_proc.call(self) : yield
181
+ ensure
182
+ self.class.paper_trail_on if paper_trail_was_enabled
183
+ end
184
+
185
+ private
186
+
187
+ def version_class
188
+ version_class_name.constantize
189
+ end
190
+
191
+ def source_version
192
+ send self.class.version_association_name
193
+ end
194
+
195
+ def record_create
196
+ if paper_trail_switched_on?
197
+ data = {
198
+ :event => paper_trail_event || 'create',
199
+ :whodunnit => PaperTrail.whodunnit
200
+ }
201
+
202
+ if changed_notably? and version_class.column_names.include?('object_changes')
203
+ data[:object_changes] = PaperTrail.serializer.dump(changes_for_paper_trail)
204
+ end
205
+ send(self.class.versions_association_name).create! merge_metadata(data)
206
+ end
207
+ end
208
+
209
+ def record_update
210
+ if paper_trail_switched_on? && changed_notably?
211
+ data = {
212
+ :event => paper_trail_event || 'update',
213
+ :object => object_to_string(item_before_change),
214
+ :whodunnit => PaperTrail.whodunnit
215
+ }
216
+ if version_class.column_names.include? 'object_changes'
217
+ data[:object_changes] = PaperTrail.serializer.dump(changes_for_paper_trail)
218
+ end
219
+ send(self.class.versions_association_name).build merge_metadata(data)
220
+ end
221
+ end
222
+
223
+ def changes_for_paper_trail
224
+ self.changes.delete_if do |key, value|
225
+ !notably_changed.include?(key)
226
+ end.tap do |changes|
227
+ self.class.serialize_attribute_changes(changes) # Use serialized value for attributes when necessary
228
+ end
229
+ end
230
+
231
+ def record_destroy
232
+ if paper_trail_switched_on? and not new_record?
233
+ version_class.create merge_metadata(:item_id => self.id,
234
+ :item_type => self.class.base_class.name,
235
+ :event => paper_trail_event || 'destroy',
236
+ :object => object_to_string(item_before_change),
237
+ :whodunnit => PaperTrail.whodunnit)
238
+ send(self.class.versions_association_name).send :load_target
239
+ end
240
+ end
241
+
242
+ def merge_metadata(data)
243
+ # First we merge the model-level metadata in `meta`.
244
+ paper_trail_options[:meta].each do |k,v|
245
+ data[k] =
246
+ if v.respond_to?(:call)
247
+ v.call(self)
248
+ elsif v.is_a?(Symbol) && respond_to?(v)
249
+ # if it is an attribute that is changing, be sure to grab the current version
250
+ if has_attribute?(v) && send("#{v}_changed?".to_sym)
251
+ send("#{v}_was".to_sym)
252
+ else
253
+ send(v)
254
+ end
255
+ else
256
+ v
257
+ end
258
+ end
259
+ # Second we merge any extra data from the controller (if available).
260
+ data.merge(PaperTrail.controller_info || {})
261
+ end
262
+
263
+ def item_before_change
264
+ previous = self.dup
265
+ # `dup` clears timestamps so we add them back.
266
+ all_timestamp_attributes.each do |column|
267
+ previous[column] = send(column) if respond_to?(column) && !send(column).nil?
268
+ end
269
+ previous.tap do |prev|
270
+ prev.id = id
271
+ changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each { |attr, before| prev[attr] = before }
272
+ end
273
+ end
274
+
275
+ def object_to_string(object)
276
+ _attrs = object.attributes.except(*self.class.paper_trail_options[:skip]).tap do |attributes|
277
+ self.class.serialize_attributes_for_paper_trail attributes
278
+ end
279
+ PaperTrail.serializer.dump(_attrs)
280
+ end
281
+
282
+ def changed_notably?
283
+ notably_changed.any?
284
+ end
285
+
286
+ def notably_changed
287
+ only = self.class.paper_trail_options[:only]
288
+ only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
289
+ end
290
+
291
+ def changed_and_not_ignored
292
+ ignore = self.class.paper_trail_options[:ignore]
293
+ skip = self.class.paper_trail_options[:skip]
294
+ changed - ignore - skip
295
+ end
296
+
297
+ def paper_trail_switched_on?
298
+ PaperTrail.enabled? && PaperTrail.enabled_for_controller? && self.class.paper_trail_enabled_for_model
299
+ end
300
+
301
+ def save_version?
302
+ if_condition = self.class.paper_trail_options[:if]
303
+ unless_condition = self.class.paper_trail_options[:unless]
304
+ (if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,17 @@
1
+ require 'active_support/json'
2
+
3
+ module PaperTrail
4
+ module Serializers
5
+ module Json
6
+ extend self # makes all instance methods become module methods as well
7
+
8
+ def load(string)
9
+ ActiveSupport::JSON.decode string
10
+ end
11
+
12
+ def dump(object)
13
+ ActiveSupport::JSON.encode object
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'yaml'
2
+
3
+ module PaperTrail
4
+ module Serializers
5
+ module Yaml
6
+ extend self # makes all instance methods become module methods as well
7
+
8
+ def load(string)
9
+ YAML.load string
10
+ end
11
+
12
+ def dump(object)
13
+ YAML.dump object
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,200 @@
1
+ module PaperTrail
2
+ class Version < ActiveRecord::Base
3
+ belongs_to :item, :polymorphic => true
4
+ validates_presence_of :event
5
+ attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes if PaperTrail.active_record_protected_attributes?
6
+
7
+ after_create :enforce_version_limit!
8
+
9
+ def self.with_item_keys(item_type, item_id)
10
+ where :item_type => item_type, :item_id => item_id
11
+ end
12
+
13
+ def self.creates
14
+ where :event => 'create'
15
+ end
16
+
17
+ def self.updates
18
+ where :event => 'update'
19
+ end
20
+
21
+ def self.destroys
22
+ where :event => 'destroy'
23
+ end
24
+
25
+ def self.not_creates
26
+ where 'event <> ?', 'create'
27
+ end
28
+
29
+ scope :subsequent, lambda { |version|
30
+ where("#{self.primary_key} > ?", version).order("#{self.primary_key} ASC")
31
+ }
32
+
33
+ scope :preceding, lambda { |version|
34
+ where("#{self.primary_key} < ?", version).order("#{self.primary_key} DESC")
35
+ }
36
+
37
+ scope :following, lambda { |timestamp|
38
+ # TODO: is this :order necessary, considering its presence on the has_many :versions association?
39
+ where("#{PaperTrail.timestamp_field} > ?", timestamp).
40
+ order("#{PaperTrail.timestamp_field} ASC, #{self.primary_key} ASC")
41
+ }
42
+
43
+ scope :between, lambda { |start_time, end_time|
44
+ where("#{PaperTrail.timestamp_field} > ? AND #{PaperTrail.timestamp_field} < ?", start_time, end_time).
45
+ order("#{PaperTrail.timestamp_field} ASC, #{self.primary_key} ASC")
46
+ }
47
+
48
+ # Restore the item from this version.
49
+ #
50
+ # This will automatically restore all :has_one associations as they were "at the time",
51
+ # if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
52
+ # to work so you can either change the lookback period (from the default 3 seconds) or
53
+ # opt out.
54
+ #
55
+ # Options:
56
+ # +:has_one+ set to `false` to opt out of has_one reification.
57
+ # set to a float to change the lookback time (check whether your db supports
58
+ # sub-second datetimes if you want them).
59
+ def reify(options = {})
60
+ without_identity_map do
61
+ options[:has_one] = 3 if options[:has_one] == true
62
+ options.reverse_merge! :has_one => false
63
+
64
+ unless object.nil?
65
+ attrs = PaperTrail.serializer.load object
66
+
67
+ # Normally a polymorphic belongs_to relationship allows us
68
+ # to get the object we belong to by calling, in this case,
69
+ # +item+. However this returns nil if +item+ has been
70
+ # destroyed, and we need to be able to retrieve destroyed
71
+ # objects.
72
+ #
73
+ # In this situation we constantize the +item_type+ to get hold of
74
+ # the class...except when the stored object's attributes
75
+ # include a +type+ key. If this is the case, the object
76
+ # we belong to is using single table inheritance and the
77
+ # +item_type+ will be the base class, not the actual subclass.
78
+ # If +type+ is present but empty, the class is the base class.
79
+
80
+ if item
81
+ model = item
82
+ # Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
83
+ (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
84
+ else
85
+ inheritance_column_name = item_type.constantize.inheritance_column
86
+ class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
87
+ klass = class_name.constantize
88
+ model = klass.new
89
+ end
90
+
91
+ model.class.unserialize_attributes_for_paper_trail attrs
92
+
93
+ # Set all the attributes in this version on the model
94
+ attrs.each do |k, v|
95
+ if model.respond_to?("#{k}=")
96
+ model[k.to_sym] = v
97
+ else
98
+ logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
99
+ end
100
+ end
101
+
102
+ model.send "#{model.class.version_association_name}=", self
103
+
104
+ unless options[:has_one] == false
105
+ reify_has_ones model, options[:has_one]
106
+ end
107
+
108
+ model
109
+ end
110
+ end
111
+ end
112
+
113
+ # Returns what changed in this version of the item. Cf. `ActiveModel::Dirty#changes`.
114
+ # Returns nil if your `versions` table does not have an `object_changes` text column.
115
+ def changeset
116
+ return nil unless self.class.column_names.include? 'object_changes'
117
+
118
+ HashWithIndifferentAccess.new(PaperTrail.serializer.load(object_changes)).tap do |changes|
119
+ item_type.constantize.unserialize_attribute_changes(changes)
120
+ end
121
+ rescue
122
+ {}
123
+ end
124
+
125
+ # Returns who put the item into the state stored in this version.
126
+ def originator
127
+ previous.try :whodunnit
128
+ end
129
+
130
+ # Returns who changed the item from the state it had in this version.
131
+ # This is an alias for `whodunnit`.
132
+ def terminator
133
+ whodunnit
134
+ end
135
+
136
+ def sibling_versions
137
+ self.class.with_item_keys(item_type, item_id)
138
+ end
139
+
140
+ def next
141
+ sibling_versions.subsequent(self).first
142
+ end
143
+
144
+ def previous
145
+ sibling_versions.preceding(self).first
146
+ end
147
+
148
+ def index
149
+ id_column = self.class.primary_key.to_sym
150
+ sibling_versions.select(id_column).order("#{id_column} ASC").map(&id_column).index(self.send(id_column))
151
+ end
152
+
153
+ private
154
+
155
+ # In Rails 3.1+, calling reify on a previous version confuses the
156
+ # IdentityMap, if enabled. This prevents insertion into the map.
157
+ def without_identity_map(&block)
158
+ if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
159
+ ActiveRecord::IdentityMap.without(&block)
160
+ else
161
+ block.call
162
+ end
163
+ end
164
+
165
+ # Restore the `model`'s has_one associations as they were when this version was
166
+ # superseded by the next (because that's what the user was looking at when they
167
+ # made the change).
168
+ #
169
+ # The `lookback` sets how many seconds before the model's change we go.
170
+ def reify_has_ones(model, lookback)
171
+ model.class.reflect_on_all_associations(:has_one).each do |assoc|
172
+ child = model.send assoc.name
173
+ if child.respond_to? :version_at
174
+ # N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
175
+ # Ideally we want the version of the child as it was just before the parent was updated...
176
+ # but until PaperTrail knows which updates are "together" (e.g. parent and child being
177
+ # updated on the same form), it's impossible to tell when the overall update started;
178
+ # and therefore impossible to know when "just before" was.
179
+ if (child_as_it_was = child.version_at(send(PaperTrail.timestamp_field) - lookback.seconds))
180
+ child_as_it_was.attributes.each do |k,v|
181
+ model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
182
+ end
183
+ else
184
+ model.send "#{assoc.name}=", nil
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # checks to see if a value has been set for the `version_limit` config option, and if so enforces it
191
+ def enforce_version_limit!
192
+ return unless PaperTrail.config.version_limit.is_a? Numeric
193
+ previous_versions = sibling_versions.not_creates
194
+ return unless previous_versions.size > PaperTrail.config.version_limit
195
+ excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
196
+ excess_previous_versions.map(&:destroy)
197
+ end
198
+
199
+ end
200
+ end