paper_trail 4.0.2 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +27 -0
  4. data/CONTRIBUTING.md +78 -5
  5. data/README.md +328 -268
  6. data/doc/bug_report_template.rb +65 -0
  7. data/gemfiles/3.0.gemfile +7 -4
  8. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +1 -1
  9. data/lib/generators/paper_trail/templates/create_versions.rb +14 -0
  10. data/lib/paper_trail.rb +11 -9
  11. data/lib/paper_trail/attributes_serialization.rb +89 -0
  12. data/lib/paper_trail/cleaner.rb +8 -1
  13. data/lib/paper_trail/config.rb +15 -18
  14. data/lib/paper_trail/frameworks/rails/controller.rb +16 -2
  15. data/lib/paper_trail/has_paper_trail.rb +102 -99
  16. data/lib/paper_trail/record_history.rb +59 -0
  17. data/lib/paper_trail/reifier.rb +270 -0
  18. data/lib/paper_trail/version_association_concern.rb +3 -1
  19. data/lib/paper_trail/version_concern.rb +60 -226
  20. data/lib/paper_trail/version_number.rb +2 -2
  21. data/paper_trail.gemspec +7 -10
  22. data/spec/models/animal_spec.rb +17 -0
  23. data/spec/models/callback_modifier_spec.rb +96 -0
  24. data/spec/models/json_version_spec.rb +20 -17
  25. data/spec/paper_trail/config_spec.rb +52 -0
  26. data/spec/spec_helper.rb +6 -0
  27. data/test/dummy/app/models/callback_modifier.rb +45 -0
  28. data/test/dummy/app/models/chapter.rb +9 -0
  29. data/test/dummy/app/models/citation.rb +5 -0
  30. data/test/dummy/app/models/paragraph.rb +5 -0
  31. data/test/dummy/app/models/quotation.rb +5 -0
  32. data/test/dummy/app/models/section.rb +6 -0
  33. data/test/dummy/config/database.postgres.yml +1 -1
  34. data/test/dummy/config/initializers/paper_trail.rb +3 -1
  35. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +33 -0
  36. data/test/dummy/db/schema.rb +27 -0
  37. data/test/test_helper.rb +36 -0
  38. data/test/unit/associations_test.rb +726 -0
  39. data/test/unit/inheritance_column_test.rb +6 -6
  40. data/test/unit/model_test.rb +62 -594
  41. data/test/unit/protected_attrs_test.rb +3 -2
  42. data/test/unit/version_test.rb +87 -69
  43. metadata +38 -2
@@ -0,0 +1,65 @@
1
+ # Use this template to report PaperTrail bugs.
2
+ # It is based on the ActiveRecord template.
3
+ # https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb
4
+ begin
5
+ require 'bundler/inline'
6
+ rescue LoadError => e
7
+ $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
8
+ raise e
9
+ end
10
+
11
+ gemfile(true) do
12
+ ruby '2.2.3'
13
+ source 'https://rubygems.org'
14
+ gem 'activerecord', '4.2.0'
15
+ gem 'minitest', '5.8.3'
16
+ gem 'paper_trail', '4.0.0', require: false
17
+ gem 'sqlite3'
18
+ end
19
+
20
+ require 'active_record'
21
+ require 'minitest/autorun'
22
+ require 'logger'
23
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
24
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
25
+ require 'paper_trail'
26
+
27
+ ActiveRecord::Schema.define do
28
+ create_table :users, force: true do |t|
29
+ t.text :first_name, null: false
30
+ t.timestamps null: false
31
+ end
32
+ create_table :versions do |t|
33
+ t.string :item_type, null: false
34
+ t.integer :item_id, null: false
35
+ t.string :event, null: false
36
+ t.string :whodunnit
37
+ t.text :object, limit: 1_073_741_823
38
+ t.text :object_changes, limit: 1_073_741_823
39
+ t.integer :transaction_id
40
+ t.datetime :created_at
41
+ end
42
+ add_index :versions, [:item_type, :item_id]
43
+ add_index :versions, [:transaction_id]
44
+
45
+ create_table :version_associations do |t|
46
+ t.integer :version_id
47
+ t.string :foreign_key_name, null: false
48
+ t.integer :foreign_key_id
49
+ end
50
+ add_index :version_associations, [:version_id]
51
+ add_index :version_associations, [:foreign_key_name, :foreign_key_id],
52
+ name: 'index_version_associations_on_foreign_key'
53
+ end
54
+
55
+ class User < ActiveRecord::Base
56
+ has_paper_trail
57
+ end
58
+
59
+ class BugTest < ActiveSupport::TestCase
60
+ def test_1
61
+ assert_difference(-> { PaperTrail::Version.count }, +1) {
62
+ User.create(first_name: "Jane")
63
+ }
64
+ end
65
+ end
@@ -4,6 +4,12 @@ gem 'activerecord', '~> 3.0'
4
4
  gem 'i18n', '~> 0.6.11'
5
5
  gem 'request_store', '~> 1.1.0'
6
6
 
7
+ # actionpack 3 depends on rack-cache ~> 1.2, but bundler seems to ignore that.
8
+ # Also rack-cache 1.3 dropped support for ruby 1.8.7. The simplest thing for now
9
+ # was to specify rack-cache ~> 1.2 here, though it should be unnecessary given
10
+ # the actionpack 3 dependency.
11
+ gem 'rack-cache', '~> 1.2.0'
12
+
7
13
  group :development, :test do
8
14
  gem 'rake', '~> 10.1.1'
9
15
  gem 'shoulda', '~> 3.5'
@@ -23,13 +29,10 @@ group :development, :test do
23
29
  # To do proper transactional testing with ActiveSupport::TestCase on MySQL
24
30
  gem 'database_cleaner', '~> 1.2.0'
25
31
 
32
+ # Allow time travel in testing. timecop is only supported after 1.9.2 but does a better cleanup at 'return'
26
33
  if RUBY_VERSION < "1.9.2"
27
34
  gem 'delorean'
28
-
29
- # rack-cache 1.3 drops ruby 1.8.7 support
30
- gem 'rack-cache', '1.2'
31
35
  else
32
- # timecop is only supported after 1.9.2 but does a better cleanup at 'return'
33
36
  gem 'timecop'
34
37
  end
35
38
 
@@ -5,6 +5,6 @@ class AddObjectChangesToVersions < ActiveRecord::Migration
5
5
  TEXT_BYTES = 1_073_741_823
6
6
 
7
7
  def change
8
- add_column :versions, :object_changes, :text, limit: TEXT_BYTES
8
+ add_column :versions, :object_changes, :text, :limit => TEXT_BYTES
9
9
  end
10
10
  end
@@ -13,6 +13,20 @@ class CreateVersions < ActiveRecord::Migration
13
13
  t.string :event, :null => false
14
14
  t.string :whodunnit
15
15
  t.text :object, :limit => TEXT_BYTES
16
+
17
+ # Known issue in MySQL: fractional second precision
18
+ # -------------------------------------------------
19
+ #
20
+ # MySQL timestamp columns do not support fractional seconds unless
21
+ # defined with "fractional seconds precision". MySQL users should manually
22
+ # add fractional seconds precision to this migration, specifically, to
23
+ # the `created_at` column.
24
+ # (https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html)
25
+ #
26
+ # MySQL users should also upgrade to rails 4.2, which is the first
27
+ # version of ActiveRecord with support for fractional seconds in MySQL.
28
+ # (https://github.com/rails/rails/pull/14359)
29
+ #
16
30
  t.datetime :created_at
17
31
  end
18
32
  add_index :versions, [:item_type, :item_id]
@@ -1,6 +1,6 @@
1
1
  require 'request_store'
2
2
 
3
- # Require core library
3
+ # Require files in lib/paper_trail, but not its subdirectories.
4
4
  Dir[File.join(File.dirname(__FILE__), 'paper_trail', '*.rb')].each do |file|
5
5
  require File.join('paper_trail', File.basename(file, '.rb'))
6
6
  end
@@ -24,11 +24,12 @@ module PaperTrail
24
24
  !!PaperTrail.config.enabled
25
25
  end
26
26
 
27
- # ActiveRecord 5 drops support for serialized attributes; for previous
28
- # versions of ActiveRecord it is supported, we have a config option
29
- # to enable it within PaperTrail.
30
27
  def self.serialized_attributes?
31
- !!PaperTrail.config.serialized_attributes && ::ActiveRecord::VERSION::MAJOR < 5
28
+ ActiveSupport::Deprecation.warn(
29
+ "PaperTrail.serialized_attributes? is deprecated without replacement " +
30
+ "and always returns false."
31
+ )
32
+ false
32
33
  end
33
34
 
34
35
  # Sets whether PaperTrail is enabled or disabled for the current request.
@@ -101,7 +102,8 @@ module PaperTrail
101
102
  end
102
103
 
103
104
  def self.active_record_protected_attributes?
104
- @active_record_protected_attributes ||= ::ActiveRecord::VERSION::MAJOR < 4 || !!defined?(ProtectedAttributes)
105
+ @active_record_protected_attributes ||= ::ActiveRecord::VERSION::MAJOR < 4 ||
106
+ !!defined?(ProtectedAttributes)
105
107
  end
106
108
 
107
109
  def self.transaction?
@@ -126,9 +128,9 @@ module PaperTrail
126
128
 
127
129
  # Returns PaperTrail's configuration object.
128
130
  def self.config
129
- @@config ||= PaperTrail::Config.instance
130
- yield @@config if block_given?
131
- @@config
131
+ @config ||= PaperTrail::Config.instance
132
+ yield @config if block_given?
133
+ @config
132
134
  end
133
135
 
134
136
  class << self
@@ -0,0 +1,89 @@
1
+ module PaperTrail
2
+ module AttributesSerialization
3
+ class NoOpAttribute
4
+ def type_cast_for_database(value)
5
+ value
6
+ end
7
+
8
+ def type_cast_from_database(data)
9
+ data
10
+ end
11
+ end
12
+ NO_OP_ATTRIBUTE = NoOpAttribute.new
13
+
14
+ class SerializedAttribute
15
+ def initialize(coder)
16
+ @coder = coder.respond_to?(:dump) ? coder : PaperTrail.serializer
17
+ end
18
+
19
+ def type_cast_for_database(value)
20
+ @coder.dump(value)
21
+ end
22
+
23
+ def type_cast_from_database(data)
24
+ @coder.load(data)
25
+ end
26
+ end
27
+
28
+ SERIALIZE, DESERIALIZE =
29
+ if ::ActiveRecord::VERSION::MAJOR >= 5
30
+ [:serialize, :deserialize]
31
+ else
32
+ [:type_cast_for_database, :type_cast_from_database]
33
+ end
34
+
35
+ if ::ActiveRecord::VERSION::STRING < '4.2'
36
+ # Backport Rails 4.2 and later's `type_for_attribute` to build
37
+ # on a common interface.
38
+ def type_for_attribute(attr_name)
39
+ serialized_attribute_types[attr_name.to_s] || NO_OP_ATTRIBUTE
40
+ end
41
+
42
+ def serialized_attribute_types
43
+ @attribute_types ||= Hash[serialized_attributes.map do |attr_name, coder|
44
+ [attr_name, SerializedAttribute.new(coder)]
45
+ end]
46
+ end
47
+ private :serialized_attribute_types
48
+ end
49
+
50
+ # Used for `Version#object` attribute.
51
+ def serialize_attributes_for_paper_trail!(attributes)
52
+ alter_attributes_for_paper_trail!(SERIALIZE, attributes)
53
+ end
54
+
55
+ def unserialize_attributes_for_paper_trail!(attributes)
56
+ alter_attributes_for_paper_trail!(DESERIALIZE, attributes)
57
+ end
58
+
59
+ def alter_attributes_for_paper_trail!(serializer, attributes)
60
+ # Don't serialize before values before inserting into columns of type
61
+ # `JSON` on `PostgreSQL` databases.
62
+ return attributes if paper_trail_version_class.object_col_is_json?
63
+
64
+ attributes.each do |key, value|
65
+ attributes[key] = type_for_attribute(key).send(serializer, value)
66
+ end
67
+ end
68
+
69
+ # Used for Version#object_changes attribute.
70
+ def serialize_attribute_changes_for_paper_trail!(changes)
71
+ alter_attribute_changes_for_paper_trail!(SERIALIZE, changes)
72
+ end
73
+
74
+ def unserialize_attribute_changes_for_paper_trail!(changes)
75
+ alter_attribute_changes_for_paper_trail!(DESERIALIZE, changes)
76
+ end
77
+
78
+ def alter_attribute_changes_for_paper_trail!(serializer, changes)
79
+ # Don't serialize before values before inserting into columns of type
80
+ # `JSON` on `PostgreSQL` databases.
81
+ return changes if paper_trail_version_class.object_changes_col_is_json?
82
+
83
+ changes.clone.each do |key, change|
84
+ type = type_for_attribute(key)
85
+ changes[key] = Array(change).map { |value| type.send(serializer, value) }
86
+ end
87
+ end
88
+ end
89
+ end
@@ -16,7 +16,7 @@ module PaperTrail
16
16
  def clean_versions!(options = {})
17
17
  options = {:keeping => 1, :date => :all}.merge(options)
18
18
  gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
19
- versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }.each do |date, _versions|
19
+ group_versions_by_date(versions).each do |date, _versions|
20
20
  # Remove the number of versions we wish to keep from the collection
21
21
  # of versions prior to destruction.
22
22
  _versions.pop(options[:keeping])
@@ -41,5 +41,12 @@ module PaperTrail
41
41
  versions = PaperTrail::Version.all if versions == PaperTrail::Version
42
42
  versions.group_by(&:item_id)
43
43
  end
44
+
45
+ # Given an array of versions, returns a hash mapping dates to arrays of
46
+ # versions.
47
+ # @api private
48
+ def group_versions_by_date(versions)
49
+ versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }
50
+ end
44
51
  end
45
52
  end
@@ -5,30 +5,26 @@ module PaperTrail
5
5
  class Config
6
6
  include Singleton
7
7
  attr_accessor :timestamp_field, :serializer, :version_limit
8
- attr_reader :serialized_attributes
9
8
  attr_writer :track_associations
10
9
 
11
10
  def initialize
12
11
  @timestamp_field = :created_at
13
12
  @serializer = PaperTrail::Serializers::YAML
13
+ end
14
14
 
15
- # This setting only defaults to false on AR 4.2+, because that's when
16
- # it was deprecated. We want it to function with older versions of
17
- # ActiveRecord by default.
18
- if ::ActiveRecord::VERSION::STRING < '4.2'
19
- @serialized_attributes = true
20
- end
15
+ def serialized_attributes
16
+ ActiveSupport::Deprecation.warn(
17
+ "PaperTrail.config.serialized_attributes is deprecated without " +
18
+ "replacement and always returns false."
19
+ )
20
+ false
21
21
  end
22
22
 
23
- def serialized_attributes=(value)
24
- if ::ActiveRecord::VERSION::MAJOR >= 5
25
- ::ActiveSupport::Deprecation.warn(
26
- "ActiveRecord 5.0 deprecated `serialized_attributes` " +
27
- "without replacement, so this PaperTrail config setting does " +
28
- "nothing with this version, and is always turned off"
29
- )
30
- end
31
- @serialized_attributes = value
23
+ def serialized_attributes=(_)
24
+ ActiveSupport::Deprecation.warn(
25
+ "PaperTrail.config.serialized_attributes= is deprecated without " +
26
+ "replacement and no longer has any effect."
27
+ )
32
28
  end
33
29
 
34
30
  def track_associations
@@ -38,9 +34,10 @@ module PaperTrail
38
34
  end
39
35
  alias_method :track_associations?, :track_associations
40
36
 
41
- # Indicates whether PaperTrail is on or off.
37
+ # Indicates whether PaperTrail is on or off. Default: true.
42
38
  def enabled
43
- PaperTrail.paper_trail_store[:paper_trail_enabled].nil? || PaperTrail.paper_trail_store[:paper_trail_enabled]
39
+ value = PaperTrail.paper_trail_store.fetch(:paper_trail_enabled, true)
40
+ value.nil? ? true : value
44
41
  end
45
42
 
46
43
  def enabled= enable
@@ -3,8 +3,22 @@ module PaperTrail
3
3
  module Controller
4
4
 
5
5
  def self.included(base)
6
- base.before_filter :set_paper_trail_enabled_for_controller
7
- base.before_filter :set_paper_trail_whodunnit, :set_paper_trail_controller_info
6
+ before = [
7
+ :set_paper_trail_enabled_for_controller,
8
+ :set_paper_trail_whodunnit,
9
+ :set_paper_trail_controller_info
10
+ ]
11
+ after = []
12
+
13
+ if base.respond_to? :before_action
14
+ # Rails 4+
15
+ before.map {|sym| base.before_action sym }
16
+ after.map {|sym| base.after_action sym }
17
+ else
18
+ # Rails 3.
19
+ before.map {|sym| base.before_filter sym }
20
+ after.map {|sym| base.after_filter sym }
21
+ end
8
22
  end
9
23
 
10
24
  protected
@@ -1,4 +1,5 @@
1
1
  require 'active_support/core_ext/object' # provides the `try` method
2
+ require 'paper_trail/attributes_serialization'
2
3
 
3
4
  module PaperTrail
4
5
  module Model
@@ -46,9 +47,22 @@ module PaperTrail
46
47
  # column if it exists. Default is true
47
48
  #
48
49
  def has_paper_trail(options = {})
50
+ options[:on] ||= [:create, :update, :destroy]
51
+
52
+ # Wrap the :on option in an array if necessary. This allows a single
53
+ # symbol to be passed in.
54
+ options[:on] = Array(options[:on])
55
+
56
+ setup_model_for_paper_trail(options)
57
+
58
+ setup_callbacks_from_options options[:on]
59
+ end
60
+
61
+ def setup_model_for_paper_trail(options = {})
49
62
  # Lazily include the instance methods so we don't clutter up
50
63
  # any more ActiveRecord models than we have to.
51
64
  send :include, InstanceMethods
65
+ send :extend, AttributesSerialization
52
66
 
53
67
  class_attribute :version_association_name
54
68
  self.version_association_name = options[:version] || :version
@@ -60,6 +74,7 @@ module PaperTrail
60
74
  self.version_class_name = options[:class_name] || 'PaperTrail::Version'
61
75
 
62
76
  class_attribute :paper_trail_options
77
+
63
78
  self.paper_trail_options = options.dup
64
79
 
65
80
  [:ignore, :skip, :only].each do |k|
@@ -87,26 +102,53 @@ module PaperTrail
87
102
  :order => self.paper_trail_version_class.timestamp_sort_order
88
103
  end
89
104
 
90
- options[:on] ||= [:create, :update, :destroy]
91
-
92
- # Wrap the :on option in an array if necessary. This allows a single
93
- # symbol to be passed in.
94
- options_on = Array(options[:on])
95
-
96
- after_create :record_create, :if => :save_version? if options_on.include?(:create)
97
- if options_on.include?(:update)
98
- before_save :reset_timestamp_attrs_for_update_if_needed!, :on => :update
99
- after_update :record_update, :if => :save_version?
100
- after_update :clear_version_instance!
101
- end
102
- after_destroy :record_destroy, :if => :save_version? if options_on.include?(:destroy)
103
-
104
105
  # Reset the transaction id when the transaction is closed.
105
106
  after_commit :reset_transaction_id
106
107
  after_rollback :reset_transaction_id
107
108
  after_rollback :clear_rolled_back_versions
108
109
  end
109
110
 
111
+ def setup_callbacks_from_options(options_on = [])
112
+ options_on.each do |option|
113
+ send "paper_trail_on_#{option}"
114
+ end
115
+ end
116
+
117
+ # Record version before or after "destroy" event
118
+ def paper_trail_on_destroy(recording_order = 'after')
119
+ unless %w[after before].include?(recording_order.to_s)
120
+ fail ArgumentError, 'recording order can only be "after" or "before"'
121
+ end
122
+
123
+ send "#{recording_order}_destroy",
124
+ :record_destroy,
125
+ :if => :save_version?
126
+
127
+ return if paper_trail_options[:on].include?(:destroy)
128
+ paper_trail_options[:on] << :destroy
129
+ end
130
+
131
+ # Record version after "update" event
132
+ def paper_trail_on_update
133
+ before_save :reset_timestamp_attrs_for_update_if_needed!,
134
+ :on => :update
135
+ after_update :record_update,
136
+ :if => :save_version?
137
+ after_update :clear_version_instance!
138
+
139
+ return if paper_trail_options[:on].include?(:update)
140
+ paper_trail_options[:on] << :update
141
+ end
142
+
143
+ # Record version after "create" event
144
+ def paper_trail_on_create
145
+ after_create :record_create,
146
+ :if => :save_version?
147
+
148
+ return if paper_trail_options[:on].include?(:create)
149
+ paper_trail_options[:on] << :create
150
+ end
151
+
110
152
  # Switches PaperTrail off for this class.
111
153
  def paper_trail_off!
112
154
  PaperTrail.enabled_for_model(self, false)
@@ -125,74 +167,6 @@ module PaperTrail
125
167
  def paper_trail_version_class
126
168
  @paper_trail_version_class ||= version_class_name.constantize
127
169
  end
128
-
129
- # Used for `Version#object` attribute.
130
- def serialize_attributes_for_paper_trail!(attributes)
131
- # Don't serialize before values before inserting into columns of type
132
- # `JSON` on `PostgreSQL` databases.
133
- return attributes if self.paper_trail_version_class.object_col_is_json?
134
-
135
- serialized_attributes.each do |key, coder|
136
- if attributes.key?(key)
137
- # Fall back to current serializer if `coder` has no `dump` method.
138
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
139
- attributes[key] = coder.dump(attributes[key])
140
- end
141
- end
142
- end
143
-
144
- # TODO: There is a lot of duplication between this and
145
- # `serialize_attributes_for_paper_trail!`.
146
- def unserialize_attributes_for_paper_trail!(attributes)
147
- # Don't serialize before values before inserting into columns of type
148
- # `JSON` on `PostgreSQL` databases.
149
- return attributes if self.paper_trail_version_class.object_col_is_json?
150
-
151
- serialized_attributes.each do |key, coder|
152
- if attributes.key?(key)
153
- # Fall back to current serializer if `coder` has no `dump` method.
154
- # TODO: Shouldn't this be `:load`?
155
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
156
- attributes[key] = coder.load(attributes[key])
157
- end
158
- end
159
- end
160
-
161
- # Used for Version#object_changes attribute.
162
- def serialize_attribute_changes_for_paper_trail!(changes)
163
- # Don't serialize before values before inserting into columns of type `JSON`
164
- # on `PostgreSQL` databases.
165
- return changes if self.paper_trail_version_class.object_changes_col_is_json?
166
-
167
- serialized_attributes.each do |key, coder|
168
- if changes.key?(key)
169
- # Fall back to current serializer if `coder` has no `dump` method.
170
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
171
- old_value, new_value = changes[key]
172
- changes[key] = [coder.dump(old_value),
173
- coder.dump(new_value)]
174
- end
175
- end
176
- end
177
-
178
- # TODO: There is a lot of duplication between this and
179
- # `serialize_attribute_changes_for_paper_trail!`.
180
- def unserialize_attribute_changes_for_paper_trail!(changes)
181
- # Don't serialize before values before inserting into columns of type
182
- # `JSON` on `PostgreSQL` databases.
183
- return changes if self.paper_trail_version_class.object_changes_col_is_json?
184
-
185
- serialized_attributes.each do |key, coder|
186
- if changes.key?(key)
187
- # Fall back to current serializer if `coder` has no `dump` method.
188
- # TODO: Shouldn't this be `:load`?
189
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
190
- old_value, new_value = changes[key]
191
- changes[key] = [coder.load(old_value),
192
- coder.load(new_value)]
193
- end
194
- end
195
- end
196
170
  end
197
171
 
198
172
  # Wrap the following methods in a module so we can include them only in the
@@ -291,7 +265,7 @@ module PaperTrail
291
265
  # `:on`, `:if`, or `:unless`.
292
266
  #
293
267
  # TODO: look into leveraging the `after_touch` callback from
294
- # `ActiveRecord` to allow the regular `touch` method go generate a version
268
+ # `ActiveRecord` to allow the regular `touch` method to generate a version
295
269
  # as normal. May make sense to switch the `record_update` method to
296
270
  # leverage an `after_update` callback anyways (likely for v4.0.0)
297
271
  def touch_with_version(name = nil)
@@ -329,9 +303,8 @@ module PaperTrail
329
303
  if respond_to?(:updated_at)
330
304
  data[PaperTrail.timestamp_field] = updated_at
331
305
  end
332
- if paper_trail_options[:save_changes] && changed_notably? && self.class.paper_trail_version_class.column_names.include?('object_changes')
333
- data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
334
- PaperTrail.serializer.dump(changes_for_paper_trail)
306
+ if pt_record_object_changes? && changed_notably?
307
+ data[:object_changes] = pt_recordable_object_changes
335
308
  end
336
309
  if self.class.paper_trail_version_class.column_names.include?('transaction_id')
337
310
  data[:transaction_id] = PaperTrail.transaction_id
@@ -344,18 +317,16 @@ module PaperTrail
344
317
 
345
318
  def record_update(force = nil)
346
319
  if paper_trail_switched_on? && (force || changed_notably?)
347
- object_attrs = object_attrs_for_paper_trail(attributes_before_change)
348
320
  data = {
349
321
  :event => paper_trail_event || 'update',
350
- :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
322
+ :object => pt_recordable_object,
351
323
  :whodunnit => PaperTrail.whodunnit
352
324
  }
353
325
  if respond_to?(:updated_at)
354
326
  data[PaperTrail.timestamp_field] = updated_at
355
327
  end
356
- if paper_trail_options[:save_changes] && self.class.paper_trail_version_class.column_names.include?('object_changes')
357
- data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
358
- PaperTrail.serializer.dump(changes_for_paper_trail)
328
+ if pt_record_object_changes?
329
+ data[:object_changes] = pt_recordable_object_changes
359
330
  end
360
331
  if self.class.paper_trail_version_class.column_names.include?('transaction_id')
361
332
  data[:transaction_id] = PaperTrail.transaction_id
@@ -366,11 +337,46 @@ module PaperTrail
366
337
  end
367
338
  end
368
339
 
340
+ # Returns a boolean indicating whether to store serialized version diffs
341
+ # in the `object_changes` column of the version record.
342
+ # @api private
343
+ def pt_record_object_changes?
344
+ paper_trail_options[:save_changes] &&
345
+ self.class.paper_trail_version_class.column_names.include?('object_changes')
346
+ end
347
+
348
+ # Returns an object which can be assigned to the `object` attribute of a
349
+ # nascent version record. If the `object` column is a postgres `json`
350
+ # column, then a hash can be used in the assignment, otherwise the column
351
+ # is a `text` column, and we must perform the serialization here, using
352
+ # `PaperTrail.serializer`.
353
+ # @api private
354
+ def pt_recordable_object
355
+ object_attrs = object_attrs_for_paper_trail(attributes_before_change)
356
+ if self.class.paper_trail_version_class.object_col_is_json?
357
+ object_attrs
358
+ else
359
+ PaperTrail.serializer.dump(object_attrs)
360
+ end
361
+ end
362
+
363
+ # Returns an object which can be assigned to the `object_changes`
364
+ # attribute of a nascent version record. If the `object_changes` column is
365
+ # a postgres `json` column, then a hash can be used in the assignment,
366
+ # otherwise the column is a `text` column, and we must perform the
367
+ # serialization here, using `PaperTrail.serializer`.
368
+ # @api private
369
+ def pt_recordable_object_changes
370
+ if self.class.paper_trail_version_class.object_changes_col_is_json?
371
+ changes_for_paper_trail
372
+ else
373
+ PaperTrail.serializer.dump(changes_for_paper_trail)
374
+ end
375
+ end
376
+
369
377
  def changes_for_paper_trail
370
378
  _changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
371
- if PaperTrail.serialized_attributes?
372
- self.class.serialize_attribute_changes_for_paper_trail!(_changes)
373
- end
379
+ self.class.serialize_attribute_changes_for_paper_trail!(_changes)
374
380
  _changes.to_hash
375
381
  end
376
382
 
@@ -397,12 +403,11 @@ module PaperTrail
397
403
 
398
404
  def record_destroy
399
405
  if paper_trail_switched_on? and not new_record?
400
- object_attrs = object_attrs_for_paper_trail(attributes_before_change)
401
406
  data = {
402
407
  :item_id => self.id,
403
408
  :item_type => self.class.base_class.name,
404
409
  :event => paper_trail_event || 'destroy',
405
- :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
410
+ :object => pt_recordable_object,
406
411
  :whodunnit => PaperTrail.whodunnit
407
412
  }
408
413
  if self.class.paper_trail_version_class.column_names.include?('transaction_id')
@@ -488,9 +493,7 @@ module PaperTrail
488
493
  # ommitting attributes to be skipped.
489
494
  def object_attrs_for_paper_trail(attributes_hash)
490
495
  attrs = attributes_hash.except(*self.paper_trail_options[:skip])
491
- if PaperTrail.serialized_attributes?
492
- self.class.serialize_attributes_for_paper_trail!(attrs)
493
- end
496
+ self.class.serialize_attributes_for_paper_trail!(attrs)
494
497
  attrs
495
498
  end
496
499