paper_trail 5.2.3 → 6.0.0

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.
@@ -103,7 +103,7 @@ module PaperTrail
103
103
  user_for_paper_trail is present, but whodunnit has not been set.
104
104
  PaperTrail no longer adds the set_paper_trail_whodunnit callback for
105
105
  you. To continue recording whodunnit, please add this before_action
106
- callback to your ApplicationController . For more information,
106
+ callback to your ApplicationController. For more information,
107
107
  please see https://git.io/vrTsk
108
108
  EOS
109
109
  end
@@ -27,3 +27,8 @@ RSpec::Matchers.define :have_a_version_with do |attributes|
27
27
  # check if the model has a version with the specified attributes
28
28
  match { |actual| actual.versions.where_object(attributes).any? }
29
29
  end
30
+
31
+ RSpec::Matchers.define :have_a_version_with_changes do |attributes|
32
+ # check if the model has a version changes with the specified attributes
33
+ match { |actual| actual.versions.where_object_changes(attributes).any? }
34
+ end
@@ -69,306 +69,15 @@ module PaperTrail
69
69
  def paper_trail
70
70
  ::PaperTrail::ModelConfig.new(self)
71
71
  end
72
-
73
- # @api private
74
- def paper_trail_deprecate(new_method, old_method = nil)
75
- old = old_method.nil? ? new_method : old_method
76
- msg = format("Use paper_trail.%s instead of %s", new_method, old)
77
- ::ActiveSupport::Deprecation.warn(msg, caller(2))
78
- end
79
-
80
- # @deprecated
81
- def paper_trail_on_destroy(*args)
82
- paper_trail_deprecate "on_destroy", "paper_trail_on_destroy"
83
- paper_trail.on_destroy(*args)
84
- end
85
-
86
- # @deprecated
87
- def paper_trail_on_update
88
- paper_trail_deprecate "on_update", "paper_trail_on_update"
89
- paper_trail.on_update
90
- end
91
-
92
- # @deprecated
93
- def paper_trail_on_create
94
- paper_trail_deprecate "on_create", "paper_trail_on_create"
95
- paper_trail.on_create
96
- end
97
-
98
- # @deprecated
99
- def paper_trail_off!
100
- paper_trail_deprecate "disable", "paper_trail_off!"
101
- paper_trail.disable
102
- end
103
-
104
- # @deprecated
105
- def paper_trail_on!
106
- paper_trail_deprecate "enable", "paper_trail_on!"
107
- paper_trail.enable
108
- end
109
-
110
- # @deprecated
111
- def paper_trail_enabled_for_model?
112
- paper_trail_deprecate "enabled?", "paper_trail_enabled_for_model?"
113
- paper_trail.enabled?
114
- end
115
-
116
- # @deprecated
117
- def paper_trail_version_class
118
- paper_trail_deprecate "version_class", "paper_trail_version_class"
119
- paper_trail.version_class
120
- end
121
72
  end
122
73
 
123
74
  # Wrap the following methods in a module so we can include them only in the
124
75
  # ActiveRecord models that declare `has_paper_trail`.
125
76
  module InstanceMethods
77
+ # @api public
126
78
  def paper_trail
127
79
  ::PaperTrail::RecordTrail.new(self)
128
80
  end
129
-
130
- # @deprecated
131
- def live?
132
- self.class.paper_trail_deprecate "live?"
133
- paper_trail.live?
134
- end
135
-
136
- # @deprecated
137
- def paper_trail_originator
138
- self.class.paper_trail_deprecate "originator", "paper_trail_originator"
139
- paper_trail.originator
140
- end
141
-
142
- # @deprecated
143
- def originator
144
- self.class.paper_trail_deprecate "originator"
145
- paper_trail.originator
146
- end
147
-
148
- # @deprecated
149
- def clear_rolled_back_versions
150
- self.class.paper_trail_deprecate "clear_rolled_back_versions"
151
- paper_trail.clear_rolled_back_versions
152
- end
153
-
154
- # @deprecated
155
- def source_version
156
- self.class.paper_trail_deprecate "source_version"
157
- paper_trail.source_version
158
- end
159
-
160
- # @deprecated
161
- def version_at(*args)
162
- self.class.paper_trail_deprecate "version_at"
163
- paper_trail.version_at(*args)
164
- end
165
-
166
- # @deprecated
167
- def versions_between(start_time, end_time, _reify_options = {})
168
- self.class.paper_trail_deprecate "versions_between"
169
- paper_trail.versions_between(start_time, end_time)
170
- end
171
-
172
- # @deprecated
173
- def previous_version
174
- self.class.paper_trail_deprecate "previous_version"
175
- paper_trail.previous_version
176
- end
177
-
178
- # @deprecated
179
- def next_version
180
- self.class.paper_trail_deprecate "next_version"
181
- paper_trail.next_version
182
- end
183
-
184
- # @deprecated
185
- def paper_trail_enabled_for_model?
186
- self.class.paper_trail_deprecate "enabled_for_model?", "paper_trail_enabled_for_model?"
187
- paper_trail.enabled_for_model?
188
- end
189
-
190
- # @deprecated
191
- def without_versioning(method = nil, &block)
192
- self.class.paper_trail_deprecate "without_versioning"
193
- paper_trail.without_versioning(method, &block)
194
- end
195
-
196
- # @deprecated
197
- def appear_as_new_record(&block)
198
- self.class.paper_trail_deprecate "appear_as_new_record"
199
- paper_trail.appear_as_new_record(&block)
200
- end
201
-
202
- # @deprecated
203
- def whodunnit(value, &block)
204
- self.class.paper_trail_deprecate "whodunnit"
205
- paper_trail.whodunnit(value, &block)
206
- end
207
-
208
- # @deprecated
209
- def touch_with_version(name = nil)
210
- self.class.paper_trail_deprecate "touch_with_version"
211
- paper_trail.touch_with_version(name)
212
- end
213
-
214
- # `record_create` is deprecated in favor of `paper_trail.record_create`,
215
- # but does not yet print a deprecation warning. When the `after_create`
216
- # callback is registered (by ModelConfig#on_create) we still refer to this
217
- # method by name, e.g.
218
- #
219
- # @model_class.after_create :record_create, if: :save_version?
220
- #
221
- # instead of using the preferred method `paper_trail.record_create`, e.g.
222
- #
223
- # @model_class.after_create { |r| r.paper_trail.record_create if r.save_version?}
224
- #
225
- # We still register the callback by name so that, if someone calls
226
- # `has_paper_trail` twice, the callback will *not* be registered twice.
227
- # Our own test suite calls `has_paper_trail` many times for the same
228
- # class.
229
- #
230
- # In the future, perhaps we should require that users only set up
231
- # PT once per class.
232
- #
233
- # @deprecated
234
- def record_create
235
- paper_trail.record_create
236
- end
237
-
238
- # See deprecation comment for `record_create`
239
- # @deprecated
240
- def record_update(force = nil)
241
- paper_trail.record_update(force)
242
- end
243
-
244
- # @deprecated
245
- def pt_record_object_changes?
246
- self.class.paper_trail_deprecate "record_object_changes?", "pt_record_object_changes?"
247
- paper_trail.record_object_changes?
248
- end
249
-
250
- # @deprecated
251
- def pt_recordable_object
252
- self.class.paper_trail_deprecate "recordable_object", "pt_recordable_object"
253
- paper_trail.recordable_object
254
- end
255
-
256
- # @deprecated
257
- def pt_recordable_object_changes
258
- self.class.paper_trail_deprecate "recordable_object_changes", "pt_recordable_object_changes"
259
- paper_trail.recordable_object_changes
260
- end
261
-
262
- # @deprecated
263
- def changes_for_paper_trail
264
- self.class.paper_trail_deprecate "changes", "changes_for_paper_trail"
265
- paper_trail.changes
266
- end
267
-
268
- # See deprecation comment for `record_create`
269
- # @deprecated
270
- def clear_version_instance!
271
- paper_trail.clear_version_instance
272
- end
273
-
274
- # See deprecation comment for `record_create`
275
- # @deprecated
276
- def reset_timestamp_attrs_for_update_if_needed!
277
- paper_trail.reset_timestamp_attrs_for_update_if_needed
278
- end
279
-
280
- # See deprecation comment for `record_create`
281
- # @deprecated
282
- def record_destroy
283
- paper_trail.record_destroy
284
- end
285
-
286
- # @deprecated
287
- def save_associations(version)
288
- self.class.paper_trail_deprecate "save_associations"
289
- paper_trail.save_associations(version)
290
- end
291
-
292
- # @deprecated
293
- def save_associations_belongs_to(version)
294
- self.class.paper_trail_deprecate "save_associations_belongs_to"
295
- paper_trail.save_associations_belongs_to(version)
296
- end
297
-
298
- # @deprecated
299
- def save_associations_has_and_belongs_to_many(version)
300
- self.class.paper_trail_deprecate(
301
- "save_associations_habtm",
302
- "save_associations_has_and_belongs_to_many"
303
- )
304
- paper_trail.save_associations_habtm(version)
305
- end
306
-
307
- # @deprecated
308
- # @api private
309
- def reset_transaction_id
310
- ::ActiveSupport::Deprecation.warn(
311
- "reset_transaction_id is deprecated, use PaperTrail.clear_transaction_id"
312
- )
313
- PaperTrail.clear_transaction_id
314
- end
315
-
316
- # @deprecated
317
- # @api private
318
- def merge_metadata(data)
319
- self.class.paper_trail_deprecate "merge_metadata"
320
- paper_trail.merge_metadata(data)
321
- end
322
-
323
- # @deprecated
324
- def attributes_before_change
325
- self.class.paper_trail_deprecate "attributes_before_change"
326
- paper_trail.attributes_before_change
327
- end
328
-
329
- # @deprecated
330
- def object_attrs_for_paper_trail
331
- self.class.paper_trail_deprecate "object_attrs_for_paper_trail"
332
- paper_trail.object_attrs_for_paper_trail
333
- end
334
-
335
- # @deprecated
336
- def changed_notably?
337
- self.class.paper_trail_deprecate "changed_notably?"
338
- paper_trail.changed_notably?
339
- end
340
-
341
- # @deprecated
342
- def ignored_attr_has_changed?
343
- self.class.paper_trail_deprecate "ignored_attr_has_changed?"
344
- paper_trail.ignored_attr_has_changed?
345
- end
346
-
347
- # @deprecated
348
- def notably_changed
349
- self.class.paper_trail_deprecate "notably_changed"
350
- paper_trail.notably_changed
351
- end
352
-
353
- # @deprecated
354
- def changed_and_not_ignored
355
- self.class.paper_trail_deprecate "changed_and_not_ignored"
356
- paper_trail.changed_and_not_ignored
357
- end
358
-
359
- # The new method is named "enabled?" for consistency.
360
- # @deprecated
361
- def paper_trail_switched_on?
362
- self.class.paper_trail_deprecate "enabled?", "paper_trail_switched_on?"
363
- paper_trail.enabled?
364
- end
365
-
366
- # @deprecated
367
- # @api private
368
- def save_version?
369
- self.class.paper_trail_deprecate "save_version?"
370
- paper_trail.save_version?
371
- end
372
81
  end
373
82
  end
374
83
  end
@@ -31,7 +31,9 @@ module PaperTrail
31
31
 
32
32
  # Adds a callback that records a version after a "create" event.
33
33
  def on_create
34
- @model_class.after_create :record_create, if: ->(m) { m.paper_trail.save_version? }
34
+ @model_class.after_create { |r|
35
+ r.paper_trail.record_create if r.paper_trail.save_version?
36
+ }
35
37
  return if @model_class.paper_trail_options[:on].include?(:create)
36
38
  @model_class.paper_trail_options[:on] << :create
37
39
  end
@@ -46,8 +48,10 @@ module PaperTrail
46
48
  ::ActiveSupport::Deprecation.warn(E_CANNOT_RECORD_AFTER_DESTROY)
47
49
  end
48
50
 
49
- @model_class.send "#{recording_order}_destroy", :record_destroy,
50
- if: ->(m) { m.paper_trail.save_version? }
51
+ @model_class.send(
52
+ "#{recording_order}_destroy",
53
+ ->(r) { r.paper_trail.record_destroy if r.paper_trail.save_version? }
54
+ )
51
55
 
52
56
  return if @model_class.paper_trail_options[:on].include?(:destroy)
53
57
  @model_class.paper_trail_options[:on] << :destroy
@@ -55,9 +59,15 @@ module PaperTrail
55
59
 
56
60
  # Adds a callback that records a version after an "update" event.
57
61
  def on_update
58
- @model_class.before_save :reset_timestamp_attrs_for_update_if_needed!, on: :update
59
- @model_class.after_update :record_update, if: ->(m) { m.paper_trail.save_version? }
60
- @model_class.after_update :clear_version_instance!
62
+ @model_class.before_save(on: :update) { |r|
63
+ r.paper_trail.reset_timestamp_attrs_for_update_if_needed
64
+ }
65
+ @model_class.after_update { |r|
66
+ r.paper_trail.record_update(nil) if r.paper_trail.save_version?
67
+ }
68
+ @model_class.after_update { |r|
69
+ r.paper_trail.clear_version_instance
70
+ }
61
71
  return if @model_class.paper_trail_options[:on].include?(:update)
62
72
  @model_class.paper_trail_options[:on] << :update
63
73
  end
@@ -133,7 +143,7 @@ module PaperTrail
133
143
  end
134
144
 
135
145
  # Adds callbacks to record changes to habtm associations such that on save
136
- # the previous version of the association (if changed) can be interpreted.
146
+ # the previous version of the association (if changed) can be reconstructed.
137
147
  def setup_callbacks_for_habtm(join_tables)
138
148
  @model_class.send :attr_accessor, :paper_trail_habtm
139
149
  @model_class.class_attribute :paper_trail_save_join_tables
@@ -26,7 +26,7 @@ module PaperTrail
26
26
  @versions.select(primary_key).order(primary_key.asc)
27
27
  else
28
28
  @versions.
29
- select([timestamp, primary_key]).
29
+ select([table[:created_at], primary_key]).
30
30
  order(@version_class.timestamp_sort_order)
31
31
  end
32
32
  end
@@ -45,13 +45,5 @@ module PaperTrail
45
45
  def table
46
46
  @version_class.arel_table
47
47
  end
48
-
49
- # @return - Arel::Attribute - Attribute representing the timestamp column
50
- # of the version table, usually named `created_at` (the rails convention)
51
- # but not always.
52
- # @api private
53
- def timestamp
54
- table[PaperTrail.timestamp_field]
55
- end
56
48
  end
57
49
  end
@@ -1,26 +1,48 @@
1
1
  module PaperTrail
2
2
  # Represents the "paper trail" for a single record.
3
3
  class RecordTrail
4
+ RAILS_GTE_5_1 = ::ActiveRecord::VERSION::MAJOR >= 5 && ::ActiveRecord::VERSION::MINOR >= 1
5
+
4
6
  def initialize(record)
5
7
  @record = record
8
+ @in_after_callback = false
6
9
  end
7
10
 
8
11
  # Utility method for reifying. Anything executed inside the block will
9
12
  # appear like a new record.
13
+ #
14
+ # > .. as best as I can tell, the purpose of
15
+ # > appear_as_new_record was to attempt to prevent the callbacks in
16
+ # > AutosaveAssociation (which is the module responsible for persisting
17
+ # > foreign key changes earlier than most people want most of the time
18
+ # > because backwards compatibility or the maintainer hates himself or
19
+ # > something) from running. By also stubbing out persisted? we can
20
+ # > actually prevent those. A more stable option might be to use suppress
21
+ # > instead, similar to the other branch in reify_has_one.
22
+ # > -Sean Griffin (https://github.com/airblade/paper_trail/pull/899)
23
+ #
10
24
  def appear_as_new_record
11
25
  @record.instance_eval {
12
26
  alias :old_new_record? :new_record?
13
27
  alias :new_record? :present?
28
+ alias :old_persisted? :persisted?
29
+ alias :persisted? :nil?
14
30
  }
15
31
  yield
16
- @record.instance_eval { alias :new_record? :old_new_record? }
32
+ @record.instance_eval {
33
+ alias :new_record? :old_new_record?
34
+ alias :persisted? :old_persisted?
35
+ }
17
36
  end
18
37
 
19
38
  def attributes_before_change
20
- changed = @record.changed_attributes.select { |k, _v|
21
- @record.class.column_names.include?(k)
22
- }
23
- @record.attributes.merge(changed)
39
+ Hash[@record.attributes.map do |k, v|
40
+ if @record.class.column_names.include?(k)
41
+ [k, attribute_in_previous_version(k)]
42
+ else
43
+ [k, v]
44
+ end
45
+ end]
24
46
  end
25
47
 
26
48
  def changed_and_not_ignored
@@ -34,7 +56,7 @@ module PaperTrail
34
56
  }
35
57
  end
36
58
  skip = @record.paper_trail_options[:skip]
37
- @record.changed - ignore - skip
59
+ changed_in_latest_version - ignore - skip
38
60
  end
39
61
 
40
62
  # Invoked after rollbacks to ensure versions records are not created for
@@ -65,7 +87,7 @@ module PaperTrail
65
87
 
66
88
  # @api private
67
89
  def changes
68
- notable_changes = @record.changes.delete_if { |k, _v|
90
+ notable_changes = changes_in_latest_version.delete_if { |k, _v|
69
91
  !notably_changed.include?(k)
70
92
  }
71
93
  AttributeSerializers::ObjectChangesAttribute.
@@ -87,7 +109,7 @@ module PaperTrail
87
109
  # changed.
88
110
  def ignored_attr_has_changed?
89
111
  ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
90
- ignored.any? && (@record.changed & ignored).any?
112
+ ignored.any? && (changed_in_latest_version & ignored).any?
91
113
  end
92
114
 
93
115
  # Returns true if this instance is the current, live one;
@@ -107,9 +129,9 @@ module PaperTrail
107
129
  # If it is an attribute that is changing in an existing object,
108
130
  # be sure to grab the current version.
109
131
  if @record.has_attribute?(v) &&
110
- @record.send("#{v}_changed?".to_sym) &&
132
+ attribute_changed_in_latest_version?(v) &&
111
133
  data[:event] != "create"
112
- @record.send("#{v}_was".to_sym)
134
+ attribute_in_previous_version(v)
113
135
  else
114
136
  @record.send(v)
115
137
  end
@@ -164,35 +186,36 @@ module PaperTrail
164
186
  end
165
187
 
166
188
  def record_create
189
+ @in_after_callback = true
167
190
  return unless enabled?
191
+ versions_assoc = @record.send(@record.class.versions_association_name)
192
+ version = versions_assoc.create! data_for_create
193
+ update_transaction_id(version)
194
+ save_associations(version)
195
+ ensure
196
+ @in_after_callback = false
197
+ end
198
+
199
+ # Returns data for record create
200
+ # @api private
201
+ def data_for_create
168
202
  data = {
169
203
  event: @record.paper_trail_event || "create",
170
204
  whodunnit: PaperTrail.whodunnit
171
205
  }
172
206
  if @record.respond_to?(:updated_at)
173
- data[PaperTrail.timestamp_field] = @record.updated_at
207
+ data[:created_at] = @record.updated_at
174
208
  end
175
209
  if record_object_changes? && changed_notably?
176
210
  data[:object_changes] = recordable_object_changes
177
211
  end
178
212
  add_transaction_id_to(data)
179
- versions_assoc = @record.send(@record.class.versions_association_name)
180
- version = versions_assoc.create! merge_metadata(data)
181
- update_transaction_id(version)
182
- save_associations(version)
213
+ merge_metadata(data)
183
214
  end
184
215
 
185
216
  def record_destroy
186
217
  if enabled? && !@record.new_record?
187
- data = {
188
- item_id: @record.id,
189
- item_type: @record.class.base_class.name,
190
- event: @record.paper_trail_event || "destroy",
191
- object: recordable_object,
192
- whodunnit: PaperTrail.whodunnit
193
- }
194
- add_transaction_id_to(data)
195
- version = @record.class.paper_trail.version_class.create(merge_metadata(data))
218
+ version = @record.class.paper_trail.version_class.create(data_for_destroy)
196
219
  if version.errors.any?
197
220
  log_version_errors(version, :destroy)
198
221
  else
@@ -204,6 +227,20 @@ module PaperTrail
204
227
  end
205
228
  end
206
229
 
230
+ # Returns data for record destroy
231
+ # @api private
232
+ def data_for_destroy
233
+ data = {
234
+ item_id: @record.id,
235
+ item_type: @record.class.base_class.name,
236
+ event: @record.paper_trail_event || "destroy",
237
+ object: recordable_object,
238
+ whodunnit: PaperTrail.whodunnit
239
+ }
240
+ add_transaction_id_to(data)
241
+ merge_metadata(data)
242
+ end
243
+
207
244
  # Returns a boolean indicating whether to store serialized version diffs
208
245
  # in the `object_changes` column of the version record.
209
246
  # @api private
@@ -213,21 +250,10 @@ module PaperTrail
213
250
  end
214
251
 
215
252
  def record_update(force)
253
+ @in_after_callback = true
216
254
  if enabled? && (force || changed_notably?)
217
- data = {
218
- event: @record.paper_trail_event || "update",
219
- object: recordable_object,
220
- whodunnit: PaperTrail.whodunnit
221
- }
222
- if @record.respond_to?(:updated_at)
223
- data[PaperTrail.timestamp_field] = @record.updated_at
224
- end
225
- if record_object_changes?
226
- data[:object_changes] = recordable_object_changes
227
- end
228
- add_transaction_id_to(data)
229
255
  versions_assoc = @record.send(@record.class.versions_association_name)
230
- version = versions_assoc.create(merge_metadata(data))
256
+ version = versions_assoc.create(data_for_update)
231
257
  if version.errors.any?
232
258
  log_version_errors(version, :update)
233
259
  else
@@ -235,6 +261,26 @@ module PaperTrail
235
261
  save_associations(version)
236
262
  end
237
263
  end
264
+ ensure
265
+ @in_after_callback = false
266
+ end
267
+
268
+ # Returns data for record update
269
+ # @api private
270
+ def data_for_update
271
+ data = {
272
+ event: @record.paper_trail_event || "update",
273
+ object: recordable_object,
274
+ whodunnit: PaperTrail.whodunnit
275
+ }
276
+ if @record.respond_to?(:updated_at)
277
+ data[:created_at] = @record.updated_at
278
+ end
279
+ if record_object_changes?
280
+ data[:object_changes] = recordable_object_changes
281
+ end
282
+ add_transaction_id_to(data)
283
+ merge_metadata(data)
238
284
  end
239
285
 
240
286
  # Returns an object which can be assigned to the `object` attribute of a
@@ -309,23 +355,19 @@ module PaperTrail
309
355
  end
310
356
  end
311
357
 
358
+ # When a record is created, updated, or destroyed, we determine what the
359
+ # HABTM associations looked like before any changes were made, by using
360
+ # the `paper_trail_habtm` data structure. Then, we create
361
+ # `VersionAssociation` records for each of the associated records.
312
362
  def save_associations_habtm(version)
313
- # Use the :added and :removed keys to extrapolate the HABTM associations
314
- # to before any changes were made
315
363
  @record.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
316
- next unless
317
- @record.class.paper_trail_save_join_tables.include?(a.name) ||
318
- a.klass.paper_trail.enabled?
319
- assoc_version_args = {
320
- version_id: version.transaction_id,
321
- foreign_key_name: a.name
322
- }
323
- assoc_ids =
324
- @record.send(a.name).to_a.map(&:id) +
325
- (@record.paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) -
326
- (@record.paper_trail_habtm.try(:[], a.name).try(:[], :added) || [])
327
- assoc_ids.each do |id|
328
- PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id))
364
+ next unless save_habtm_association?(a)
365
+ habtm_assoc_ids(a).each do |id|
366
+ PaperTrail::VersionAssociation.create(
367
+ version_id: version.transaction_id,
368
+ foreign_key_name: a.name,
369
+ foreign_key_id: id
370
+ )
329
371
  end
330
372
  end
331
373
  end
@@ -360,7 +402,7 @@ module PaperTrail
360
402
  attributes.each { |column|
361
403
  @record.send(:write_attribute, column, current_time)
362
404
  }
363
- @record.record_update(true) unless will_record_after_update?
405
+ record_update(true) unless will_record_after_update?
364
406
  @record.save!(validate: false)
365
407
  end
366
408
 
@@ -376,9 +418,7 @@ module PaperTrail
376
418
  # Returns the objects (not Versions) as they were between the given times.
377
419
  def versions_between(start_time, end_time)
378
420
  versions = send(@record.class.versions_association_name).between(start_time, end_time)
379
- versions.collect { |version|
380
- version_at(version.send(PaperTrail.timestamp_field))
381
- }
421
+ versions.collect { |version| version_at(version.created_at) }
382
422
  end
383
423
 
384
424
  # Executes the given method or block without creating a new version.
@@ -416,6 +456,51 @@ module PaperTrail
416
456
  data[:transaction_id] = PaperTrail.transaction_id
417
457
  end
418
458
 
459
+ # @api private
460
+ def attribute_changed_in_latest_version?(attr_name)
461
+ if @in_after_callback && RAILS_GTE_5_1
462
+ @record.saved_change_to_attribute?(attr_name.to_s)
463
+ else
464
+ @record.attribute_changed?(attr_name.to_s)
465
+ end
466
+ end
467
+
468
+ # @api private
469
+ def attribute_in_previous_version(attr_name)
470
+ if @in_after_callback && RAILS_GTE_5_1
471
+ @record.attribute_before_last_save(attr_name.to_s)
472
+ else
473
+ @record.attribute_was(attr_name.to_s)
474
+ end
475
+ end
476
+
477
+ # @api private
478
+ def changed_in_latest_version
479
+ if @in_after_callback && RAILS_GTE_5_1
480
+ @record.saved_changes.keys
481
+ else
482
+ @record.changed
483
+ end
484
+ end
485
+
486
+ # @api private
487
+ def changes_in_latest_version
488
+ if @in_after_callback && RAILS_GTE_5_1
489
+ @record.saved_changes
490
+ else
491
+ @record.changes
492
+ end
493
+ end
494
+
495
+ # Given a HABTM association, returns an array of ids.
496
+ # @api private
497
+ def habtm_assoc_ids(habtm_assoc)
498
+ current = @record.send(habtm_assoc.name).to_a.map(&:id) # TODO: `pluck` would use less memory
499
+ removed = @record.paper_trail_habtm.try(:[], habtm_assoc.name).try(:[], :removed) || []
500
+ added = @record.paper_trail_habtm.try(:[], habtm_assoc.name).try(:[], :added) || []
501
+ current + removed - added
502
+ end
503
+
419
504
  def log_version_errors(version, action)
420
505
  version.logger.warn(
421
506
  "Unable to create version for #{action} of #{@record.class.name}" +
@@ -423,6 +508,13 @@ module PaperTrail
423
508
  )
424
509
  end
425
510
 
511
+ # Returns true if the given HABTM association should be saved.
512
+ # @api private
513
+ def save_habtm_association?(assoc)
514
+ @record.class.paper_trail_save_join_tables.include?(assoc.name) ||
515
+ assoc.klass.paper_trail.enabled?
516
+ end
517
+
426
518
  # Returns true if `save` will cause `record_update`
427
519
  # to be called via the `after_update` callback.
428
520
  def will_record_after_update?