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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/CHANGELOG.md +27 -0
- data/CONTRIBUTING.md +78 -5
- data/README.md +328 -268
- data/doc/bug_report_template.rb +65 -0
- data/gemfiles/3.0.gemfile +7 -4
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +1 -1
- data/lib/generators/paper_trail/templates/create_versions.rb +14 -0
- data/lib/paper_trail.rb +11 -9
- data/lib/paper_trail/attributes_serialization.rb +89 -0
- data/lib/paper_trail/cleaner.rb +8 -1
- data/lib/paper_trail/config.rb +15 -18
- data/lib/paper_trail/frameworks/rails/controller.rb +16 -2
- data/lib/paper_trail/has_paper_trail.rb +102 -99
- data/lib/paper_trail/record_history.rb +59 -0
- data/lib/paper_trail/reifier.rb +270 -0
- data/lib/paper_trail/version_association_concern.rb +3 -1
- data/lib/paper_trail/version_concern.rb +60 -226
- data/lib/paper_trail/version_number.rb +2 -2
- data/paper_trail.gemspec +7 -10
- data/spec/models/animal_spec.rb +17 -0
- data/spec/models/callback_modifier_spec.rb +96 -0
- data/spec/models/json_version_spec.rb +20 -17
- data/spec/paper_trail/config_spec.rb +52 -0
- data/spec/spec_helper.rb +6 -0
- data/test/dummy/app/models/callback_modifier.rb +45 -0
- data/test/dummy/app/models/chapter.rb +9 -0
- data/test/dummy/app/models/citation.rb +5 -0
- data/test/dummy/app/models/paragraph.rb +5 -0
- data/test/dummy/app/models/quotation.rb +5 -0
- data/test/dummy/app/models/section.rb +6 -0
- data/test/dummy/config/database.postgres.yml +1 -1
- data/test/dummy/config/initializers/paper_trail.rb +3 -1
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +33 -0
- data/test/dummy/db/schema.rb +27 -0
- data/test/test_helper.rb +36 -0
- data/test/unit/associations_test.rb +726 -0
- data/test/unit/inheritance_column_test.rb +6 -6
- data/test/unit/model_test.rb +62 -594
- data/test/unit/protected_attrs_test.rb +3 -2
- data/test/unit/version_test.rb +87 -69
- 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
|
data/gemfiles/3.0.gemfile
CHANGED
@@ -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
|
|
@@ -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]
|
data/lib/paper_trail.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'request_store'
|
2
2
|
|
3
|
-
# Require
|
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
|
-
|
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 ||
|
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
|
-
|
130
|
-
yield
|
131
|
-
|
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
|
data/lib/paper_trail/cleaner.rb
CHANGED
@@ -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
|
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
|
data/lib/paper_trail/config.rb
CHANGED
@@ -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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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=(
|
24
|
-
|
25
|
-
|
26
|
-
"
|
27
|
-
|
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
|
-
|
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
|
-
|
7
|
-
|
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
|
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
|
333
|
-
data[:object_changes] =
|
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 =>
|
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
|
357
|
-
data[:object_changes] =
|
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
|
-
|
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 =>
|
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
|
-
|
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
|
|