paper_trail 5.2.3 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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?