paper_trail 3.0.9 → 4.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -2
  4. data/.travis.yml +5 -0
  5. data/CHANGELOG.md +37 -23
  6. data/README.md +170 -63
  7. data/gemfiles/3.0.gemfile +10 -4
  8. data/lib/generators/paper_trail/install_generator.rb +19 -3
  9. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +11 -0
  10. data/lib/generators/paper_trail/templates/create_version_associations.rb +17 -0
  11. data/lib/paper_trail.rb +24 -4
  12. data/lib/paper_trail/cleaner.rb +3 -3
  13. data/lib/paper_trail/config.rb +17 -0
  14. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +7 -0
  15. data/lib/paper_trail/frameworks/rails.rb +1 -0
  16. data/lib/paper_trail/frameworks/rspec.rb +5 -0
  17. data/lib/paper_trail/has_paper_trail.rb +112 -38
  18. data/lib/paper_trail/version_association_concern.rb +13 -0
  19. data/lib/paper_trail/version_concern.rb +145 -38
  20. data/lib/paper_trail/version_number.rb +3 -3
  21. data/paper_trail.gemspec +11 -4
  22. data/spec/generators/install_generator_spec.rb +4 -4
  23. data/spec/models/fluxor_spec.rb +19 -0
  24. data/spec/models/gadget_spec.rb +10 -10
  25. data/spec/models/joined_version_spec.rb +9 -9
  26. data/spec/models/post_with_status_spec.rb +3 -3
  27. data/spec/models/version_spec.rb +49 -71
  28. data/spec/models/widget_spec.rb +124 -71
  29. data/spec/modules/version_concern_spec.rb +8 -8
  30. data/spec/modules/version_number_spec.rb +16 -16
  31. data/spec/paper_trail_spec.rb +17 -17
  32. data/spec/rails_helper.rb +34 -0
  33. data/spec/requests/articles_spec.rb +11 -11
  34. data/spec/spec_helper.rb +77 -36
  35. data/test/dummy/app/models/animal.rb +0 -2
  36. data/test/dummy/app/models/book.rb +4 -0
  37. data/test/dummy/app/models/customer.rb +4 -0
  38. data/test/dummy/app/models/editor.rb +4 -0
  39. data/test/dummy/app/models/editorship.rb +5 -0
  40. data/test/dummy/app/models/line_item.rb +4 -0
  41. data/test/dummy/app/models/order.rb +5 -0
  42. data/test/dummy/app/models/person.rb +1 -1
  43. data/test/dummy/app/models/post.rb +0 -1
  44. data/test/dummy/app/models/song.rb +0 -20
  45. data/test/dummy/app/models/widget.rb +4 -0
  46. data/test/dummy/config/application.rb +3 -0
  47. data/test/dummy/config/initializers/paper_trail.rb +1 -1
  48. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +41 -0
  49. data/test/dummy/db/schema.rb +95 -25
  50. data/test/dummy/public/404.html +26 -0
  51. data/test/dummy/public/422.html +26 -0
  52. data/test/dummy/public/500.html +26 -0
  53. data/test/dummy/public/favicon.ico +0 -0
  54. data/test/dummy/public/javascripts/application.js +2 -0
  55. data/test/dummy/public/javascripts/controls.js +965 -0
  56. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  57. data/test/dummy/public/javascripts/effects.js +1123 -0
  58. data/test/dummy/public/javascripts/rails.js +175 -0
  59. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  60. data/test/test_helper.rb +2 -2
  61. data/test/time_travel_helper.rb +15 -0
  62. data/test/unit/model_test.rb +613 -185
  63. data/test/unit/serializer_test.rb +3 -3
  64. metadata +104 -54
  65. data/spec/models/animal_spec.rb +0 -19
  66. data/test/dummy/public/javascripts/prototype.js +0 -6001
data/gemfiles/3.0.gemfile CHANGED
@@ -6,7 +6,7 @@ gem 'i18n', '~> 0.6.11'
6
6
  group :development, :test do
7
7
  gem 'rake', '~> 10.1.1'
8
8
  gem 'shoulda', '~> 3.5'
9
- gem 'ffaker', '<= 1.31.0'
9
+ gem 'ffaker', '>= 1.15'
10
10
 
11
11
  # Testing of Rails
12
12
  gem 'railties', '~> 3.0'
@@ -16,16 +16,23 @@ group :development, :test do
16
16
  gem 'rack-test', '>= 0.6'
17
17
 
18
18
  # RSpec testing
19
- gem 'rspec-rails', '~> 2.14'
19
+ gem 'rspec-rails', '~> 3.1.0'
20
20
  gem 'generator_spec'
21
21
 
22
22
  # To do proper transactional testing with ActiveSupport::TestCase on MySQL
23
23
  gem 'database_cleaner', '~> 1.2.0'
24
24
 
25
+ # Allow time travel in testing. timecop is only supported after 1.9.2 but does a better cleanup at 'return'
26
+ if RUBY_VERSION < "1.9.2"
27
+ gem 'delorean'
28
+ else
29
+ gem 'timecop'
30
+ end
31
+
25
32
  platforms :ruby do
26
33
  gem 'sqlite3', '~> 1.2'
27
34
  gem 'mysql2', '~> 0.3'
28
- gem 'pg', '~> 0.17.1'
35
+ gem 'pg', '~> 0.17'
29
36
  end
30
37
 
31
38
  platforms :jruby, :ruby_18 do
@@ -39,6 +46,5 @@ group :development, :test do
39
46
  gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3'
40
47
  gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3'
41
48
  gem 'activerecord-jdbcmysql-adapter', '~> 1.3'
42
- gem 'activerecord-jdbc-adapter', '1.3.15'
43
49
  end
44
50
  end
@@ -6,17 +6,33 @@ module PaperTrail
6
6
  include ::Rails::Generators::Migration
7
7
 
8
8
  source_root File.expand_path('../templates', __FILE__)
9
- class_option :with_changes, :type => :boolean, :default => false, :desc => "Store changeset (diff) with each version"
9
+ class_option :with_changes, :type => :boolean, :default => false,
10
+ :desc => "Store changeset (diff) with each version"
11
+ class_option :with_associations, :type => :boolean, :default => false,
12
+ :desc => "Store transactional IDs to support association restoration"
10
13
 
11
14
  desc 'Generates (but does not run) a migration to add a versions table.'
12
15
 
13
16
  def create_migration_file
14
- migration_template 'create_versions.rb', 'db/migrate/create_versions.rb'
15
- migration_template 'add_object_changes_to_versions.rb', 'db/migrate/add_object_changes_to_versions.rb' if options.with_changes?
17
+ add_paper_trail_migration('create_versions')
18
+ add_paper_trail_migration('add_object_changes_to_versions') if options.with_changes?
19
+ if options.with_associations?
20
+ add_paper_trail_migration('create_version_associations')
21
+ add_paper_trail_migration('add_transaction_id_column_to_versions')
22
+ end
16
23
  end
17
24
 
18
25
  def self.next_migration_number(dirname)
19
26
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
20
27
  end
28
+
29
+ protected
30
+ def add_paper_trail_migration(template)
31
+ migration_dir = File.expand_path('db/migrate')
32
+
33
+ if !self.class.migration_exists?(migration_dir, template)
34
+ migration_template "#{template}.rb", "db/migrate/#{template}.rb"
35
+ end
36
+ end
21
37
  end
22
38
  end
@@ -0,0 +1,11 @@
1
+ class AddTransactionIdColumnToVersions < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :versions, :transaction_id, :integer
4
+ add_index :versions, [:transaction_id]
5
+ end
6
+
7
+ def self.down
8
+ remove_index :versions, [:transaction_id]
9
+ remove_column :versions, :transaction_id
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ class CreateVersionAssociations < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :version_associations do |t|
4
+ t.integer :version_id
5
+ t.string :foreign_key_name, :null => false
6
+ t.integer :foreign_key_id
7
+ end
8
+ add_index :version_associations, [:version_id]
9
+ add_index :version_associations, [:foreign_key_name, :foreign_key_id], :name => 'index_version_associations_on_foreign_key'
10
+ end
11
+
12
+ def self.down
13
+ remove_index :version_associations, [:version_id]
14
+ remove_index :version_associations, :name => 'index_version_associations_on_foreign_key'
15
+ drop_table :version_associations
16
+ end
17
+ end
data/lib/paper_trail.rb CHANGED
@@ -22,6 +22,13 @@ module PaperTrail
22
22
  !!PaperTrail.config.enabled
23
23
  end
24
24
 
25
+ # ActiveRecord 5 drops support for serialized attributes; for previous
26
+ # versions of ActiveRecord it is supported, we have a config option
27
+ # to enable it within PaperTrail.
28
+ def self.serialized_attributes?
29
+ !!PaperTrail.config.serialized_attributes && ::ActiveRecord::VERSION::MAJOR < 5
30
+ end
31
+
25
32
  # Sets whether PaperTrail is enabled or disabled for the current request.
26
33
  def self.enabled_for_controller=(value)
27
34
  paper_trail_store[:request_enabled_for_controller] = value
@@ -94,6 +101,18 @@ module PaperTrail
94
101
  @active_record_protected_attributes ||= ::ActiveRecord::VERSION::MAJOR < 4 || !!defined?(ProtectedAttributes)
95
102
  end
96
103
 
104
+ def self.transaction?
105
+ ActiveRecord::Base.connection.open_transactions > 0
106
+ end
107
+
108
+ def self.transaction_id
109
+ paper_trail_store[:transaction_id]
110
+ end
111
+
112
+ def self.transaction_id=(id)
113
+ paper_trail_store[:transaction_id] = id
114
+ end
115
+
97
116
  private
98
117
 
99
118
  # Thread-safe hash to hold PaperTrail's data.
@@ -125,8 +144,9 @@ ActiveSupport.on_load(:active_record) do
125
144
  end
126
145
 
127
146
  # Require frameworks
128
- require 'paper_trail/frameworks/active_record'
129
147
  require 'paper_trail/frameworks/sinatra'
130
- require 'paper_trail/frameworks/rails' if defined? Rails
131
- require 'paper_trail/frameworks/rspec' if defined? RSpec::Core
132
- require 'paper_trail/frameworks/cucumber' if defined? World
148
+ if defined? Rails
149
+ require 'paper_trail/frameworks/rails'
150
+ else
151
+ require 'paper_trail/frameworks/active_record'
152
+ end
@@ -12,10 +12,10 @@ module PaperTrail
12
12
  def clean_versions!(options = {})
13
13
  options = {:keeping => 1, :date => :all}.merge(options)
14
14
  gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
15
- versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }.each do |date, versions|
15
+ versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }.each do |date, _versions|
16
16
  # remove the number of versions we wish to keep from the collection of versions prior to destruction
17
- versions.pop(options[:keeping])
18
- versions.map(&:destroy)
17
+ _versions.pop(options[:keeping])
18
+ _versions.map(&:destroy)
19
19
  end
20
20
  end
21
21
  end
@@ -4,11 +4,28 @@ module PaperTrail
4
4
  class Config
5
5
  include Singleton
6
6
  attr_accessor :enabled, :timestamp_field, :serializer, :version_limit
7
+ attr_reader :serialized_attributes
7
8
 
8
9
  def initialize
9
10
  @enabled = true # Indicates whether PaperTrail is on or off.
10
11
  @timestamp_field = :created_at
11
12
  @serializer = PaperTrail::Serializers::YAML
13
+
14
+ # This setting only defaults to false on AR 4.2+, because that's when
15
+ # it was deprecated. We want it to function with older versions of
16
+ # ActiveRecord by default.
17
+ if ::ActiveRecord::VERSION::STRING < '4.2'
18
+ @serialized_attributes = true
19
+ end
20
+ end
21
+
22
+ def serialized_attributes=(value)
23
+ if ::ActiveRecord::VERSION::MAJOR >= 5
24
+ warn("DEPRECATED: ActiveRecord 5.0 deprecated `serialized_attributes` " +
25
+ "without replacement, so this PaperTrail config setting does " +
26
+ "nothing with this version, and is always turned off")
27
+ end
28
+ @serialized_attributes = value
12
29
  end
13
30
  end
14
31
  end
@@ -0,0 +1,7 @@
1
+ require 'paper_trail/version_association_concern'
2
+
3
+ module PaperTrail
4
+ class VersionAssociation < ::ActiveRecord::Base
5
+ include PaperTrail::VersionAssociationConcern
6
+ end
7
+ end
@@ -1,4 +1,5 @@
1
1
  require 'paper_trail/frameworks/rails/controller'
2
+ require 'paper_trail/frameworks/rails/engine'
2
3
 
3
4
  module PaperTrail
4
5
  module Rails
@@ -22,3 +22,8 @@ RSpec::Matchers.define :be_versioned do
22
22
  # check to see if the model has `has_paper_trail` declared on it
23
23
  match { |actual| actual.kind_of?(::PaperTrail::Model::InstanceMethods) }
24
24
  end
25
+
26
+ RSpec::Matchers.define :have_a_version_with do |attributes|
27
+ # check if the model has a version with the specified attributes
28
+ match { |actual| actual.versions.where_object(attributes).any? }
29
+ end
@@ -1,5 +1,3 @@
1
- require 'active_support/core_ext/object' # provides the `try` method
2
-
3
1
  module PaperTrail
4
2
  module Model
5
3
 
@@ -76,10 +74,15 @@ module PaperTrail
76
74
  after_create :record_create, :if => :save_version? if options_on.empty? || options_on.include?(:create)
77
75
  if options_on.empty? || options_on.include?(:update)
78
76
  before_save :reset_timestamp_attrs_for_update_if_needed!, :on => :update
79
- before_update :record_update, :if => :save_version?
77
+ after_update :record_update, :if => :save_version?
80
78
  after_update :clear_version_instance!
81
79
  end
82
80
  after_destroy :record_destroy, :if => :save_version? if options_on.empty? || options_on.include?(:destroy)
81
+
82
+ # Reset the transaction id when the transaction is closed
83
+ after_commit :reset_transaction_id
84
+ after_rollback :reset_transaction_id
85
+ after_rollback :clear_rolled_back_versions
83
86
  end
84
87
 
85
88
  # Switches PaperTrail off for this class.
@@ -103,6 +106,7 @@ module PaperTrail
103
106
  end
104
107
 
105
108
  def paper_trail_enabled_for_model?
109
+ return false unless self.include?(PaperTrail::Model::InstanceMethods)
106
110
  PaperTrail.enabled_for_model?(self)
107
111
  end
108
112
 
@@ -117,7 +121,8 @@ module PaperTrail
117
121
 
118
122
  serialized_attributes.each do |key, coder|
119
123
  if attributes.key?(key)
120
- coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
124
+ # Fall back to current serializer if `coder` has no `dump` method
125
+ coder = PaperTrail.serializer unless coder.respond_to?(:dump)
121
126
  attributes[key] = coder.dump(attributes[key])
122
127
  end
123
128
  end
@@ -129,7 +134,7 @@ module PaperTrail
129
134
 
130
135
  serialized_attributes.each do |key, coder|
131
136
  if attributes.key?(key)
132
- coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump)
137
+ coder = PaperTrail.serializer unless coder.respond_to?(:dump)
133
138
  attributes[key] = coder.load(attributes[key])
134
139
  end
135
140
  end
@@ -142,7 +147,8 @@ module PaperTrail
142
147
 
143
148
  serialized_attributes.each do |key, coder|
144
149
  if changes.key?(key)
145
- coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
150
+ # Fall back to current serializer if `coder` has no `dump` method
151
+ coder = PaperTrail.serializer unless coder.respond_to?(:dump)
146
152
  old_value, new_value = changes[key]
147
153
  changes[key] = [coder.dump(old_value),
148
154
  coder.dump(new_value)]
@@ -156,7 +162,7 @@ module PaperTrail
156
162
 
157
163
  serialized_attributes.each do |key, coder|
158
164
  if changes.key?(key)
159
- coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump)
165
+ coder = PaperTrail.serializer unless coder.respond_to?(:dump)
160
166
  old_value, new_value = changes[key]
161
167
  changes[key] = [coder.load(old_value),
162
168
  coder.load(new_value)]
@@ -175,13 +181,14 @@ module PaperTrail
175
181
  end
176
182
 
177
183
  # Returns who put the object into its current state.
178
- def paper_trail_originator
184
+ def originator
179
185
  (source_version || send(self.class.versions_association_name).last).try(:whodunnit)
180
186
  end
181
187
 
182
- def originator
183
- warn "DEPRECATED: use `paper_trail_originator` instead of `originator`. Support for `originator` will be removed in PaperTrail 4.0"
184
- self.paper_trail_originator
188
+ # Invoked after rollbacks to ensure versions records are not created
189
+ # for changes that never actually took place
190
+ def clear_rolled_back_versions
191
+ send(self.class.versions_association_name).reload
185
192
  end
186
193
 
187
194
  # Returns the object (not a Version) as it was at the given timestamp.
@@ -228,6 +235,16 @@ module PaperTrail
228
235
  self.class.paper_trail_on! if paper_trail_was_enabled
229
236
  end
230
237
 
238
+ # Utility method for reifying. Anything executed inside the block will appear like a new record
239
+ def appear_as_new_record
240
+ instance_eval {
241
+ alias :old_new_record? :new_record?
242
+ alias :new_record? :present?
243
+ }
244
+ yield
245
+ instance_eval { alias :new_record? :old_new_record? }
246
+ end
247
+
231
248
  # Temporarily overwrites the value of whodunnit and then executes the provided block.
232
249
  def whodunnit(value)
233
250
  raise ArgumentError, 'expected to receive a block' unless block_given?
@@ -263,15 +280,20 @@ module PaperTrail
263
280
  def record_create
264
281
  if paper_trail_switched_on?
265
282
  data = {
266
- :event => paper_trail_event || 'create',
267
- :whodunnit => PaperTrail.whodunnit
283
+ :event => paper_trail_event || 'create',
284
+ :whodunnit => PaperTrail.whodunnit,
285
+ :transaction_id => PaperTrail.transaction_id
268
286
  }
269
-
287
+ if respond_to?(:created_at)
288
+ data[PaperTrail.timestamp_field] = created_at
289
+ end
270
290
  if changed_notably? and self.class.paper_trail_version_class.column_names.include?('object_changes')
271
291
  data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
272
292
  PaperTrail.serializer.dump(changes_for_paper_trail)
273
293
  end
274
- send(self.class.versions_association_name).create! merge_metadata(data)
294
+ version = send(self.class.versions_association_name).create! merge_metadata(data)
295
+ set_transaction_id(version)
296
+ save_associations(version)
275
297
  end
276
298
  end
277
299
 
@@ -279,23 +301,30 @@ module PaperTrail
279
301
  if paper_trail_switched_on? && changed_notably?
280
302
  object_attrs = object_attrs_for_paper_trail(item_before_change)
281
303
  data = {
282
- :event => paper_trail_event || 'update',
283
- :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
284
- :whodunnit => PaperTrail.whodunnit
304
+ :event => paper_trail_event || 'update',
305
+ :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
306
+ :whodunnit => PaperTrail.whodunnit,
307
+ :transaction_id => PaperTrail.transaction_id
285
308
  }
286
-
309
+ if respond_to?(:updated_at)
310
+ data[PaperTrail.timestamp_field] = updated_at
311
+ end
287
312
  if self.class.paper_trail_version_class.column_names.include?('object_changes')
288
313
  data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
289
314
  PaperTrail.serializer.dump(changes_for_paper_trail)
290
315
  end
291
- send(self.class.versions_association_name).build merge_metadata(data)
316
+ version = send(self.class.versions_association_name).create merge_metadata(data)
317
+ set_transaction_id(version)
318
+ save_associations(version)
292
319
  end
293
320
  end
294
321
 
295
322
  def changes_for_paper_trail
296
- self.changes.delete_if do |key, value|
297
- !notably_changed.include?(key)
298
- end.tap { |changes| self.class.serialize_attribute_changes(changes) }
323
+ _changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
324
+ if PaperTrail.serialized_attributes?
325
+ self.class.serialize_attribute_changes(_changes)
326
+ end
327
+ _changes.to_hash
299
328
  end
300
329
 
301
330
  # Invoked via`after_update` callback for when a previous version is reified and then saved
@@ -305,24 +334,59 @@ module PaperTrail
305
334
 
306
335
  def reset_timestamp_attrs_for_update_if_needed!
307
336
  return if self.live? # invoked via callback when a user attempts to persist a reified `Version`
308
- timestamp_attributes_for_update_in_model.each { |column| send("reset_#{column}!") }
337
+ timestamp_attributes_for_update_in_model.each do |column|
338
+ # ActiveRecord 4.2 deprecated `reset_column!` in favor of `restore_column!`
339
+ if respond_to?("restore_#{column}!")
340
+ send("restore_#{column}!")
341
+ else
342
+ send("reset_#{column}!")
343
+ end
344
+ end
309
345
  end
310
346
 
311
347
  def record_destroy
312
348
  if paper_trail_switched_on? and not new_record?
313
349
  object_attrs = object_attrs_for_paper_trail(item_before_change)
314
350
  data = {
315
- :item_id => self.id,
316
- :item_type => self.class.base_class.name,
317
- :event => paper_trail_event || 'destroy',
318
- :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
319
- :whodunnit => PaperTrail.whodunnit
351
+ :item_id => self.id,
352
+ :item_type => self.class.base_class.name,
353
+ :event => paper_trail_event || 'destroy',
354
+ :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
355
+ :whodunnit => PaperTrail.whodunnit,
356
+ :transaction_id => PaperTrail.transaction_id
320
357
  }
321
- send("#{self.class.version_association_name}=", self.class.paper_trail_version_class.create(merge_metadata(data)))
358
+ version = self.class.paper_trail_version_class.create(merge_metadata(data))
359
+ send("#{self.class.version_association_name}=", version)
322
360
  send(self.class.versions_association_name).send :load_target
361
+ set_transaction_id(version)
362
+ save_associations(version)
363
+ end
364
+ end
365
+
366
+ def save_associations(version)
367
+ self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
368
+ if assoc.klass.paper_trail_enabled_for_model?
369
+ PaperTrail::VersionAssociation.create(
370
+ :version_id => version.id,
371
+ :foreign_key_name => assoc.foreign_key,
372
+ :foreign_key_id => self.send(assoc.foreign_key)
373
+ )
374
+ end
375
+ end
376
+ end
377
+
378
+ def set_transaction_id(version)
379
+ if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
380
+ PaperTrail.transaction_id = version.id
381
+ version.transaction_id = version.id
382
+ version.save
323
383
  end
324
384
  end
325
385
 
386
+ def reset_transaction_id
387
+ PaperTrail.transaction_id = nil
388
+ end
389
+
326
390
  def merge_metadata(data)
327
391
  # First we merge the model-level metadata in `meta`.
328
392
  paper_trail_options[:meta].each do |k,v|
@@ -345,8 +409,16 @@ module PaperTrail
345
409
  end
346
410
 
347
411
  def item_before_change
348
- attributes.tap do |prev|
349
- enums = self.respond_to?(:defined_enums) ? self.defined_enums : {}
412
+ previous = self.dup
413
+ # `dup` clears timestamps so we add them back.
414
+ all_timestamp_attributes.each do |column|
415
+ if self.class.column_names.include?(column.to_s) and not send("#{column}_was").nil?
416
+ previous[column] = send("#{column}_was")
417
+ end
418
+ end
419
+ enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {}
420
+ previous.tap do |prev|
421
+ prev.id = id # `dup` clears the `id` so we add that back
350
422
  changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
351
423
  before = enums[attr][before] if enums[attr]
352
424
  prev[attr] = before
@@ -356,16 +428,18 @@ module PaperTrail
356
428
 
357
429
  # returns hash of attributes (with appropriate attributes serialized),
358
430
  # ommitting attributes to be skipped
359
- def object_attrs_for_paper_trail(attributes_hash)
360
- attributes_hash.except(*self.paper_trail_options[:skip]).tap do |attributes|
361
- self.class.serialize_attributes_for_paper_trail(attributes)
431
+ def object_attrs_for_paper_trail(object)
432
+ attrs = object.attributes.except(*self.paper_trail_options[:skip])
433
+ if PaperTrail.serialized_attributes?
434
+ self.class.serialize_attributes_for_paper_trail(attrs)
362
435
  end
436
+ attrs
363
437
  end
364
438
 
365
439
  # This method is invoked in order to determine whether it is appropriate to generate a new version instance.
366
- # In ActiveRecord 4, the way ActiveModel::Dirty handles the `changes` hash has changed so that
367
- # `timestamp_attributes_for_update` get inserted PRIOR to the persistence happening, so we must accomodate
368
- # this by checking to ensure attributes other than those ignored and update timestamps are really being modified.
440
+ # Because we are now using `after_(create/update/etc)` callbacks, we need to go out of our way to
441
+ # ensure that during updates timestamp attributes are not acknowledged as a notable changes
442
+ # to raise false positives when attributes are ignored.
369
443
  def changed_notably?
370
444
  if self.paper_trail_options[:ignore].any? && (changed & self.paper_trail_options[:ignore]).any?
371
445
  (notably_changed - timestamp_attributes_for_update_in_model.map(&:to_s)).any?