paper_trail 3.0.6 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.rspec +1 -2
- data/.travis.yml +14 -5
- data/CHANGELOG.md +215 -8
- data/CONTRIBUTING.md +84 -0
- data/README.md +922 -502
- data/Rakefile +2 -2
- data/doc/bug_report_template.rb +65 -0
- data/gemfiles/ar3.gemfile +61 -0
- data/lib/generators/paper_trail/install_generator.rb +22 -3
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +6 -1
- data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +11 -0
- data/lib/generators/paper_trail/templates/create_version_associations.rb +17 -0
- data/lib/generators/paper_trail/templates/create_versions.rb +22 -1
- data/lib/paper_trail.rb +52 -22
- data/lib/paper_trail/attributes_serialization.rb +89 -0
- data/lib/paper_trail/cleaner.rb +32 -15
- data/lib/paper_trail/config.rb +35 -2
- data/lib/paper_trail/frameworks/active_record.rb +4 -5
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +7 -0
- data/lib/paper_trail/frameworks/rails.rb +1 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +27 -11
- data/lib/paper_trail/frameworks/rspec.rb +5 -0
- data/lib/paper_trail/frameworks/sinatra.rb +3 -1
- data/lib/paper_trail/has_paper_trail.rb +304 -148
- data/lib/paper_trail/record_history.rb +59 -0
- data/lib/paper_trail/reifier.rb +270 -0
- data/lib/paper_trail/serializers/json.rb +13 -2
- data/lib/paper_trail/serializers/yaml.rb +16 -2
- data/lib/paper_trail/version_association_concern.rb +15 -0
- data/lib/paper_trail/version_concern.rb +160 -122
- data/lib/paper_trail/version_number.rb +3 -3
- data/paper_trail.gemspec +22 -9
- data/spec/generators/install_generator_spec.rb +4 -4
- data/spec/models/animal_spec.rb +36 -0
- data/spec/models/boolit_spec.rb +48 -0
- data/spec/models/callback_modifier_spec.rb +96 -0
- data/spec/models/fluxor_spec.rb +19 -0
- data/spec/models/gadget_spec.rb +14 -12
- data/spec/models/joined_version_spec.rb +9 -9
- data/spec/models/json_version_spec.rb +103 -0
- data/spec/models/kitchen/banana_spec.rb +14 -0
- data/spec/models/not_on_update_spec.rb +19 -0
- data/spec/models/post_with_status_spec.rb +3 -3
- data/spec/models/skipper_spec.rb +46 -0
- data/spec/models/thing_spec.rb +11 -0
- data/spec/models/version_spec.rb +195 -44
- data/spec/models/widget_spec.rb +136 -76
- data/spec/modules/paper_trail_spec.rb +27 -0
- data/spec/modules/version_concern_spec.rb +8 -8
- data/spec/modules/version_number_spec.rb +16 -16
- data/spec/paper_trail/config_spec.rb +52 -0
- data/spec/paper_trail_spec.rb +17 -17
- data/spec/rails_helper.rb +34 -0
- data/spec/requests/articles_spec.rb +10 -14
- data/spec/spec_helper.rb +81 -34
- data/spec/support/alt_db_init.rb +1 -1
- data/test/dummy/app/controllers/application_controller.rb +1 -1
- data/test/dummy/app/controllers/articles_controller.rb +4 -1
- data/test/dummy/app/models/animal.rb +2 -0
- data/test/dummy/app/models/book.rb +4 -0
- data/test/dummy/app/models/boolit.rb +4 -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/customer.rb +4 -0
- data/test/dummy/app/models/editor.rb +4 -0
- data/test/dummy/app/models/editorship.rb +5 -0
- data/test/dummy/app/models/fruit.rb +5 -0
- data/test/dummy/app/models/kitchen/banana.rb +5 -0
- data/test/dummy/app/models/line_item.rb +4 -0
- data/test/dummy/app/models/not_on_update.rb +4 -0
- data/test/dummy/app/models/order.rb +5 -0
- data/test/dummy/app/models/paragraph.rb +5 -0
- data/test/dummy/app/models/person.rb +13 -3
- data/test/dummy/app/models/post.rb +0 -1
- data/test/dummy/app/models/quotation.rb +5 -0
- data/test/dummy/app/models/section.rb +6 -0
- data/test/dummy/app/models/skipper.rb +6 -0
- data/test/dummy/app/models/song.rb +20 -0
- data/test/dummy/app/models/thing.rb +3 -0
- data/test/dummy/app/models/whatchamajigger.rb +4 -0
- data/test/dummy/app/models/widget.rb +5 -0
- data/test/dummy/app/versions/json_version.rb +3 -0
- data/test/dummy/app/versions/kitchen/banana_version.rb +5 -0
- data/test/dummy/config/application.rb +6 -0
- data/test/dummy/config/database.postgres.yml +1 -1
- data/test/dummy/config/environments/test.rb +5 -1
- data/test/dummy/config/initializers/paper_trail.rb +6 -1
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +143 -3
- data/test/dummy/db/schema.rb +169 -25
- data/test/functional/controller_test.rb +4 -2
- data/test/functional/modular_sinatra_test.rb +1 -1
- data/test/functional/sinatra_test.rb +1 -1
- data/test/paper_trail_test.rb +7 -0
- data/test/test_helper.rb +38 -2
- data/test/time_travel_helper.rb +15 -0
- data/test/unit/associations_test.rb +726 -0
- data/test/unit/inheritance_column_test.rb +6 -6
- data/test/unit/model_test.rb +109 -125
- data/test/unit/protected_attrs_test.rb +4 -3
- data/test/unit/serializer_test.rb +6 -6
- data/test/unit/serializers/json_test.rb +17 -4
- data/test/unit/serializers/yaml_test.rb +5 -1
- data/test/unit/version_test.rb +87 -69
- metadata +172 -75
- data/gemfiles/3.0.gemfile +0 -42
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -175
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
data/lib/paper_trail/cleaner.rb
CHANGED
@@ -1,35 +1,52 @@
|
|
1
1
|
module PaperTrail
|
2
2
|
module Cleaner
|
3
|
-
# Destroys all but the most recent version(s) for items on a given date
|
3
|
+
# Destroys all but the most recent version(s) for items on a given date
|
4
|
+
# (or on all dates). Useful for deleting drafts.
|
4
5
|
#
|
5
6
|
# Options:
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
7
|
+
#
|
8
|
+
# - :keeping - An `integer` indicating the number of versions to be kept for
|
9
|
+
# each item per date. Defaults to `1`.
|
10
|
+
# - :date - Should either be a `Date` object specifying which date to
|
11
|
+
# destroy versions for or `:all`, which will specify that all dates
|
12
|
+
# should be cleaned. Defaults to `:all`.
|
13
|
+
# - :item_id - The `id` for the item to be cleaned on, or `nil`, which
|
14
|
+
# causes all items to be cleaned. Defaults to `nil`.
|
15
|
+
#
|
12
16
|
def clean_versions!(options = {})
|
13
17
|
options = {:keeping => 1, :date => :all}.merge(options)
|
14
18
|
gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
|
15
|
-
versions
|
16
|
-
#
|
17
|
-
versions.
|
18
|
-
|
19
|
+
group_versions_by_date(versions).each do |date, _versions|
|
20
|
+
# Remove the number of versions we wish to keep from the collection
|
21
|
+
# of versions prior to destruction.
|
22
|
+
_versions.pop(options[:keeping])
|
23
|
+
_versions.map(&:destroy)
|
19
24
|
end
|
20
25
|
end
|
21
26
|
end
|
22
27
|
|
23
28
|
private
|
24
29
|
|
25
|
-
# Returns a hash of versions grouped by the `item_id` attribute formatted
|
26
|
-
# If `item_id` or `date` is
|
30
|
+
# Returns a hash of versions grouped by the `item_id` attribute formatted
|
31
|
+
# like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
|
32
|
+
# set, versions will be narrowed to those pointing at items with those ids
|
33
|
+
# that were created on specified date.
|
27
34
|
def gather_versions(item_id = nil, date = :all)
|
28
|
-
raise "`date` argument must receive a Timestamp or `:all`" unless date == :all || date.respond_to?(:to_date)
|
35
|
+
raise ArgumentError.new("`date` argument must receive a Timestamp or `:all`") unless date == :all || date.respond_to?(:to_date)
|
29
36
|
versions = item_id ? PaperTrail::Version.where(:item_id => item_id) : PaperTrail::Version
|
30
37
|
versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
|
31
|
-
|
38
|
+
|
39
|
+
# If `versions` has not been converted to an ActiveRecord::Relation yet,
|
40
|
+
# do so now.
|
41
|
+
versions = PaperTrail::Version.all if versions == PaperTrail::Version
|
32
42
|
versions.group_by(&:item_id)
|
33
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
|
34
51
|
end
|
35
52
|
end
|
data/lib/paper_trail/config.rb
CHANGED
@@ -1,14 +1,47 @@
|
|
1
1
|
require 'singleton'
|
2
|
+
require 'paper_trail/serializers/yaml'
|
2
3
|
|
3
4
|
module PaperTrail
|
4
5
|
class Config
|
5
6
|
include Singleton
|
6
|
-
attr_accessor :
|
7
|
+
attr_accessor :timestamp_field, :serializer, :version_limit
|
8
|
+
attr_writer :track_associations
|
7
9
|
|
8
10
|
def initialize
|
9
|
-
@enabled = true # Indicates whether PaperTrail is on or off.
|
10
11
|
@timestamp_field = :created_at
|
11
12
|
@serializer = PaperTrail::Serializers::YAML
|
12
13
|
end
|
14
|
+
|
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
|
+
end
|
22
|
+
|
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
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def track_associations
|
31
|
+
@track_associations.nil? ?
|
32
|
+
PaperTrail::VersionAssociation.table_exists? :
|
33
|
+
@track_associations
|
34
|
+
end
|
35
|
+
alias_method :track_associations?, :track_associations
|
36
|
+
|
37
|
+
# Indicates whether PaperTrail is on or off. Default: true.
|
38
|
+
def enabled
|
39
|
+
value = PaperTrail.paper_trail_store.fetch(:paper_trail_enabled, true)
|
40
|
+
value.nil? ? true : value
|
41
|
+
end
|
42
|
+
|
43
|
+
def enabled= enable
|
44
|
+
PaperTrail.paper_trail_store[:paper_trail_enabled] = enable
|
45
|
+
end
|
13
46
|
end
|
14
47
|
end
|
@@ -1,5 +1,4 @@
|
|
1
|
-
# This file only needs to be loaded if the gem is being used outside of Rails,
|
2
|
-
# the model(s) will get loaded in via the `Rails::Engine
|
3
|
-
|
4
|
-
|
5
|
-
end
|
1
|
+
# This file only needs to be loaded if the gem is being used outside of Rails,
|
2
|
+
# since otherwise the model(s) will get loaded in via the `Rails::Engine`.
|
3
|
+
require "paper_trail/frameworks/active_record/models/paper_trail/version_association"
|
4
|
+
require "paper_trail/frameworks/active_record/models/paper_trail/version"
|
@@ -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
|
@@ -36,24 +50,26 @@ module PaperTrail
|
|
36
50
|
#
|
37
51
|
# The columns `ip` and `user_agent` must exist in your `versions` # table.
|
38
52
|
#
|
39
|
-
# Use the `:meta` option to
|
40
|
-
# to store any extra
|
53
|
+
# Use the `:meta` option to
|
54
|
+
# `PaperTrail::Model::ClassMethods.has_paper_trail` to store any extra
|
55
|
+
# model-level data you need.
|
41
56
|
def info_for_paper_trail
|
42
57
|
{}
|
43
58
|
end
|
44
59
|
|
45
|
-
# Returns `true` (default) or `false` depending on whether PaperTrail
|
46
|
-
# be active for the current request.
|
60
|
+
# Returns `true` (default) or `false` depending on whether PaperTrail
|
61
|
+
# should be active for the current request.
|
47
62
|
#
|
48
|
-
# Override this method in your controller to specify when PaperTrail
|
49
|
-
# be off.
|
63
|
+
# Override this method in your controller to specify when PaperTrail
|
64
|
+
# should be off.
|
50
65
|
def paper_trail_enabled_for_controller
|
51
66
|
::PaperTrail.enabled?
|
52
67
|
end
|
53
68
|
|
54
69
|
private
|
55
70
|
|
56
|
-
# Tells PaperTrail whether versions should be saved in the current
|
71
|
+
# Tells PaperTrail whether versions should be saved in the current
|
72
|
+
# request.
|
57
73
|
def set_paper_trail_enabled_for_controller
|
58
74
|
::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
|
59
75
|
end
|
@@ -63,8 +79,8 @@ module PaperTrail
|
|
63
79
|
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
64
80
|
end
|
65
81
|
|
66
|
-
# Tells PaperTrail any information from the controller you want
|
67
|
-
#
|
82
|
+
# Tells PaperTrail any information from the controller you want to store
|
83
|
+
# alongside any changes that occur.
|
68
84
|
def set_paper_trail_controller_info
|
69
85
|
::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
70
86
|
end
|
@@ -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
|
@@ -3,8 +3,10 @@ require 'active_support/core_ext/object' # provides the `try` method
|
|
3
3
|
module PaperTrail
|
4
4
|
module Sinatra
|
5
5
|
|
6
|
-
# Register this module inside your Sinatra application to gain access to
|
6
|
+
# Register this module inside your Sinatra application to gain access to
|
7
|
+
# controller-level methods used by PaperTrail.
|
7
8
|
def self.registered(app)
|
9
|
+
app.use RequestStore::Middleware
|
8
10
|
app.helpers self
|
9
11
|
app.before { set_paper_trail_whodunnit }
|
10
12
|
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'active_support/core_ext/object' # provides the `try` method
|
2
|
+
require 'paper_trail/attributes_serialization'
|
3
|
+
|
1
4
|
module PaperTrail
|
2
5
|
module Model
|
3
6
|
|
@@ -6,34 +9,60 @@ module PaperTrail
|
|
6
9
|
end
|
7
10
|
|
8
11
|
module ClassMethods
|
9
|
-
# Declare this in your model to track every create, update, and destroy.
|
10
|
-
# the model is available in the `versions` association.
|
12
|
+
# Declare this in your model to track every create, update, and destroy.
|
13
|
+
# Each version of the model is available in the `versions` association.
|
11
14
|
#
|
12
15
|
# Options:
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# :
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# :
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
# :
|
31
|
-
#
|
32
|
-
#
|
16
|
+
#
|
17
|
+
# - :on - The events to track (optional; defaults to all of them). Set
|
18
|
+
# to an array of `:create`, `:update`, `:destroy` as desired.
|
19
|
+
# - :class_name - The name of a custom Version class. This class should
|
20
|
+
# inherit from `PaperTrail::Version`.
|
21
|
+
# - :ignore - An array of attributes for which a new `Version` will not be
|
22
|
+
# created if only they change. It can also aceept a Hash as an
|
23
|
+
# argument where the key is the attribute to ignore (a `String` or
|
24
|
+
# `Symbol`), which will only be ignored if the value is a `Proc` which
|
25
|
+
# returns truthily.
|
26
|
+
# - :if, :unless - Procs that allow to specify conditions when to save
|
27
|
+
# versions for an object.
|
28
|
+
# - :only - Inverse of `ignore`. A new `Version` will be created only
|
29
|
+
# for these attributes if supplied it can also aceept a Hash as an
|
30
|
+
# argument where the key is the attribute to track (a `String` or
|
31
|
+
# `Symbol`), which will only be counted if the value is a `Proc` which
|
32
|
+
# returns truthily.
|
33
|
+
# - :skip - Fields to ignore completely. As with `ignore`, updates to
|
34
|
+
# these fields will not create a new `Version`. In addition, these
|
35
|
+
# fields will not be included in the serialized versions of the object
|
36
|
+
# whenever a new `Version` is created.
|
37
|
+
# - :meta - A hash of extra data to store. You must add a column to the
|
38
|
+
# `versions` table for each key. Values are objects or procs (which
|
39
|
+
# are called with `self`, i.e. the model with the paper trail). See
|
40
|
+
# `PaperTrail::Controller.info_for_paper_trail` for how to store data
|
41
|
+
# from the controller.
|
42
|
+
# - :versions - The name to use for the versions association. Default
|
43
|
+
# is `:versions`.
|
44
|
+
# - :version - The name to use for the method which returns the version
|
45
|
+
# the instance was reified from. Default is `:version`.
|
46
|
+
# - :save_changes - Whether or not to save changes to the object_changes
|
47
|
+
# column if it exists. Default is true
|
48
|
+
#
|
33
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 = {})
|
34
62
|
# Lazily include the instance methods so we don't clutter up
|
35
63
|
# any more ActiveRecord models than we have to.
|
36
64
|
send :include, InstanceMethods
|
65
|
+
send :extend, AttributesSerialization
|
37
66
|
|
38
67
|
class_attribute :version_association_name
|
39
68
|
self.version_association_name = options[:version] || :version
|
@@ -45,6 +74,7 @@ module PaperTrail
|
|
45
74
|
self.version_class_name = options[:class_name] || 'PaperTrail::Version'
|
46
75
|
|
47
76
|
class_attribute :paper_trail_options
|
77
|
+
|
48
78
|
self.paper_trail_options = options.dup
|
49
79
|
|
50
80
|
[:ignore, :skip, :only].each do |k|
|
@@ -53,13 +83,15 @@ module PaperTrail
|
|
53
83
|
end
|
54
84
|
|
55
85
|
paper_trail_options[:meta] ||= {}
|
86
|
+
paper_trail_options[:save_changes] = true if paper_trail_options[:save_changes].nil?
|
56
87
|
|
57
88
|
class_attribute :versions_association_name
|
58
89
|
self.versions_association_name = options[:versions] || :versions
|
59
90
|
|
60
91
|
attr_accessor :paper_trail_event
|
61
92
|
|
62
|
-
|
93
|
+
# `has_many` syntax for specifying order uses a lambda in Rails 4
|
94
|
+
if ::ActiveRecord::VERSION::MAJOR >= 4
|
63
95
|
has_many self.versions_association_name,
|
64
96
|
lambda { order(model.timestamp_sort_order) },
|
65
97
|
:class_name => self.version_class_name, :as => :item
|
@@ -70,96 +102,79 @@ module PaperTrail
|
|
70
102
|
:order => self.paper_trail_version_class.timestamp_sort_order
|
71
103
|
end
|
72
104
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
before_update :record_update, :if => :save_version?
|
78
|
-
after_update :clear_version_instance!
|
79
|
-
end
|
80
|
-
after_destroy :record_destroy, :if => :save_version? if options_on.empty? || options_on.include?(:destroy)
|
81
|
-
end
|
82
|
-
|
83
|
-
# Switches PaperTrail off for this class.
|
84
|
-
def paper_trail_off!
|
85
|
-
PaperTrail.enabled_for_model(self, false)
|
105
|
+
# Reset the transaction id when the transaction is closed.
|
106
|
+
after_commit :reset_transaction_id
|
107
|
+
after_rollback :reset_transaction_id
|
108
|
+
after_rollback :clear_rolled_back_versions
|
86
109
|
end
|
87
110
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
111
|
+
def setup_callbacks_from_options(options_on = [])
|
112
|
+
options_on.each do |option|
|
113
|
+
send "paper_trail_on_#{option}"
|
114
|
+
end
|
91
115
|
end
|
92
116
|
|
93
|
-
#
|
94
|
-
def
|
95
|
-
|
96
|
-
|
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
|
97
122
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
123
|
+
if recording_order.to_s == 'after' and
|
124
|
+
Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5")
|
125
|
+
if ::ActiveRecord::Base.belongs_to_required_by_default
|
126
|
+
::ActiveSupport::Deprecation.warn(
|
127
|
+
"paper_trail_on_destroy(:after) is incompatible with ActiveRecord " +
|
128
|
+
"belongs_to_required_by_default and has no effect. Please use :before " +
|
129
|
+
"or disable belongs_to_required_by_default."
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
102
133
|
|
103
|
-
|
104
|
-
PaperTrail.enabled_for_model?(self)
|
105
|
-
end
|
134
|
+
send "#{recording_order}_destroy", :record_destroy, :if => :save_version?
|
106
135
|
|
107
|
-
|
108
|
-
|
136
|
+
return if paper_trail_options[:on].include?(:destroy)
|
137
|
+
paper_trail_options[:on] << :destroy
|
109
138
|
end
|
110
139
|
|
111
|
-
#
|
112
|
-
def
|
113
|
-
|
114
|
-
|
140
|
+
# Record version after "update" event
|
141
|
+
def paper_trail_on_update
|
142
|
+
before_save :reset_timestamp_attrs_for_update_if_needed!,
|
143
|
+
:on => :update
|
144
|
+
after_update :record_update,
|
145
|
+
:if => :save_version?
|
146
|
+
after_update :clear_version_instance!
|
115
147
|
|
116
|
-
|
117
|
-
|
118
|
-
coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
|
119
|
-
attributes[key] = coder.dump(attributes[key])
|
120
|
-
end
|
121
|
-
end
|
148
|
+
return if paper_trail_options[:on].include?(:update)
|
149
|
+
paper_trail_options[:on] << :update
|
122
150
|
end
|
123
151
|
|
124
|
-
|
125
|
-
|
126
|
-
|
152
|
+
# Record version after "create" event
|
153
|
+
def paper_trail_on_create
|
154
|
+
after_create :record_create,
|
155
|
+
:if => :save_version?
|
127
156
|
|
128
|
-
|
129
|
-
|
130
|
-
coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump)
|
131
|
-
attributes[key] = coder.load(attributes[key])
|
132
|
-
end
|
133
|
-
end
|
157
|
+
return if paper_trail_options[:on].include?(:create)
|
158
|
+
paper_trail_options[:on] << :create
|
134
159
|
end
|
135
160
|
|
136
|
-
#
|
137
|
-
def
|
138
|
-
|
139
|
-
|
161
|
+
# Switches PaperTrail off for this class.
|
162
|
+
def paper_trail_off!
|
163
|
+
PaperTrail.enabled_for_model(self, false)
|
164
|
+
end
|
140
165
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
old_value, new_value = changes[key]
|
145
|
-
changes[key] = [coder.dump(old_value),
|
146
|
-
coder.dump(new_value)]
|
147
|
-
end
|
148
|
-
end
|
166
|
+
# Switches PaperTrail on for this class.
|
167
|
+
def paper_trail_on!
|
168
|
+
PaperTrail.enabled_for_model(self, true)
|
149
169
|
end
|
150
170
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
171
|
+
def paper_trail_enabled_for_model?
|
172
|
+
return false unless self.include?(PaperTrail::Model::InstanceMethods)
|
173
|
+
PaperTrail.enabled_for_model?(self)
|
174
|
+
end
|
154
175
|
|
155
|
-
|
156
|
-
|
157
|
-
coder = PaperTrail::Serializers::YAML unless coder.respond_to?(:dump)
|
158
|
-
old_value, new_value = changes[key]
|
159
|
-
changes[key] = [coder.load(old_value),
|
160
|
-
coder.load(new_value)]
|
161
|
-
end
|
162
|
-
end
|
176
|
+
def paper_trail_version_class
|
177
|
+
@paper_trail_version_class ||= version_class_name.constantize
|
163
178
|
end
|
164
179
|
end
|
165
180
|
|
@@ -173,10 +188,21 @@ module PaperTrail
|
|
173
188
|
end
|
174
189
|
|
175
190
|
# Returns who put the object into its current state.
|
176
|
-
def
|
191
|
+
def paper_trail_originator
|
177
192
|
(source_version || send(self.class.versions_association_name).last).try(:whodunnit)
|
178
193
|
end
|
179
194
|
|
195
|
+
def originator
|
196
|
+
::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
|
197
|
+
self.paper_trail_originator
|
198
|
+
end
|
199
|
+
|
200
|
+
# Invoked after rollbacks to ensure versions records are not created
|
201
|
+
# for changes that never actually took place
|
202
|
+
def clear_rolled_back_versions
|
203
|
+
send(self.class.versions_association_name).reload
|
204
|
+
end
|
205
|
+
|
180
206
|
# Returns the object (not a Version) as it was at the given timestamp.
|
181
207
|
def version_at(timestamp, reify_options={})
|
182
208
|
# Because a version stores how its object looked *before* the change,
|
@@ -221,7 +247,19 @@ module PaperTrail
|
|
221
247
|
self.class.paper_trail_on! if paper_trail_was_enabled
|
222
248
|
end
|
223
249
|
|
224
|
-
#
|
250
|
+
# Utility method for reifying. Anything executed inside the block will
|
251
|
+
# appear like a new record.
|
252
|
+
def appear_as_new_record
|
253
|
+
instance_eval {
|
254
|
+
alias :old_new_record? :new_record?
|
255
|
+
alias :new_record? :present?
|
256
|
+
}
|
257
|
+
yield
|
258
|
+
instance_eval { alias :new_record? :old_new_record? }
|
259
|
+
end
|
260
|
+
|
261
|
+
# Temporarily overwrites the value of whodunnit and then executes the
|
262
|
+
# provided block.
|
225
263
|
def whodunnit(value)
|
226
264
|
raise ArgumentError, 'expected to receive a block' unless block_given?
|
227
265
|
current_whodunnit = PaperTrail.whodunnit
|
@@ -231,11 +269,14 @@ module PaperTrail
|
|
231
269
|
PaperTrail.whodunnit = current_whodunnit
|
232
270
|
end
|
233
271
|
|
234
|
-
#
|
272
|
+
# Mimics the `touch` method from `ActiveRecord::Persistence`, but also
|
273
|
+
# creates a version. A version is created regardless of options such as
|
274
|
+
# `:on`, `:if`, or `:unless`.
|
235
275
|
#
|
236
|
-
# TODO:
|
237
|
-
#
|
238
|
-
#
|
276
|
+
# TODO: look into leveraging the `after_touch` callback from
|
277
|
+
# `ActiveRecord` to allow the regular `touch` method to generate a version
|
278
|
+
# as normal. May make sense to switch the `record_update` method to
|
279
|
+
# leverage an `after_update` callback anyways (likely for v4.0.0)
|
239
280
|
def touch_with_version(name = nil)
|
240
281
|
raise ActiveRecordError, "can not touch on a new record object" unless persisted?
|
241
282
|
|
@@ -244,11 +285,20 @@ module PaperTrail
|
|
244
285
|
current_time = current_time_from_proper_timezone
|
245
286
|
|
246
287
|
attributes.each { |column| write_attribute(column, current_time) }
|
247
|
-
|
288
|
+
|
289
|
+
record_update(true) unless will_record_after_update?
|
290
|
+
save!(:validate => false)
|
248
291
|
end
|
249
292
|
|
250
293
|
private
|
251
294
|
|
295
|
+
# Returns true if `save` will cause `record_update`
|
296
|
+
# to be called via the `after_update` callback.
|
297
|
+
def will_record_after_update?
|
298
|
+
on = paper_trail_options[:on]
|
299
|
+
on.nil? || on.include?(:update)
|
300
|
+
end
|
301
|
+
|
252
302
|
def source_version
|
253
303
|
send self.class.version_association_name
|
254
304
|
end
|
@@ -259,63 +309,162 @@ module PaperTrail
|
|
259
309
|
:event => paper_trail_event || 'create',
|
260
310
|
:whodunnit => PaperTrail.whodunnit
|
261
311
|
}
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
312
|
+
if respond_to?(:updated_at)
|
313
|
+
data[PaperTrail.timestamp_field] = updated_at
|
314
|
+
end
|
315
|
+
if pt_record_object_changes? && changed_notably?
|
316
|
+
data[:object_changes] = pt_recordable_object_changes
|
317
|
+
end
|
318
|
+
if self.class.paper_trail_version_class.column_names.include?('transaction_id')
|
319
|
+
data[:transaction_id] = PaperTrail.transaction_id
|
266
320
|
end
|
267
|
-
send(self.class.versions_association_name).create! merge_metadata(data)
|
321
|
+
version = send(self.class.versions_association_name).create! merge_metadata(data)
|
322
|
+
set_transaction_id(version)
|
323
|
+
save_associations(version)
|
268
324
|
end
|
269
325
|
end
|
270
326
|
|
271
|
-
def record_update
|
272
|
-
if paper_trail_switched_on? && changed_notably?
|
273
|
-
object_attrs = object_attrs_for_paper_trail(item_before_change)
|
327
|
+
def record_update(force = nil)
|
328
|
+
if paper_trail_switched_on? && (force || changed_notably?)
|
274
329
|
data = {
|
275
330
|
:event => paper_trail_event || 'update',
|
276
|
-
:object =>
|
331
|
+
:object => pt_recordable_object,
|
277
332
|
:whodunnit => PaperTrail.whodunnit
|
278
333
|
}
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
334
|
+
if respond_to?(:updated_at)
|
335
|
+
data[PaperTrail.timestamp_field] = updated_at
|
336
|
+
end
|
337
|
+
if pt_record_object_changes?
|
338
|
+
data[:object_changes] = pt_recordable_object_changes
|
339
|
+
end
|
340
|
+
if self.class.paper_trail_version_class.column_names.include?('transaction_id')
|
341
|
+
data[:transaction_id] = PaperTrail.transaction_id
|
283
342
|
end
|
284
|
-
send(self.class.versions_association_name).
|
343
|
+
version = send(self.class.versions_association_name).create merge_metadata(data)
|
344
|
+
set_transaction_id(version)
|
345
|
+
save_associations(version)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Returns a boolean indicating whether to store serialized version diffs
|
350
|
+
# in the `object_changes` column of the version record.
|
351
|
+
# @api private
|
352
|
+
def pt_record_object_changes?
|
353
|
+
paper_trail_options[:save_changes] &&
|
354
|
+
self.class.paper_trail_version_class.column_names.include?('object_changes')
|
355
|
+
end
|
356
|
+
|
357
|
+
# Returns an object which can be assigned to the `object` attribute of a
|
358
|
+
# nascent version record. If the `object` column is a postgres `json`
|
359
|
+
# column, then a hash can be used in the assignment, otherwise the column
|
360
|
+
# is a `text` column, and we must perform the serialization here, using
|
361
|
+
# `PaperTrail.serializer`.
|
362
|
+
# @api private
|
363
|
+
def pt_recordable_object
|
364
|
+
object_attrs = object_attrs_for_paper_trail(attributes_before_change)
|
365
|
+
if self.class.paper_trail_version_class.object_col_is_json?
|
366
|
+
object_attrs
|
367
|
+
else
|
368
|
+
PaperTrail.serializer.dump(object_attrs)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Returns an object which can be assigned to the `object_changes`
|
373
|
+
# attribute of a nascent version record. If the `object_changes` column is
|
374
|
+
# a postgres `json` column, then a hash can be used in the assignment,
|
375
|
+
# otherwise the column is a `text` column, and we must perform the
|
376
|
+
# serialization here, using `PaperTrail.serializer`.
|
377
|
+
# @api private
|
378
|
+
def pt_recordable_object_changes
|
379
|
+
if self.class.paper_trail_version_class.object_changes_col_is_json?
|
380
|
+
changes_for_paper_trail
|
381
|
+
else
|
382
|
+
PaperTrail.serializer.dump(changes_for_paper_trail)
|
285
383
|
end
|
286
384
|
end
|
287
385
|
|
288
386
|
def changes_for_paper_trail
|
289
|
-
|
290
|
-
|
291
|
-
|
387
|
+
_changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
|
388
|
+
self.class.serialize_attribute_changes_for_paper_trail!(_changes)
|
389
|
+
_changes.to_hash
|
292
390
|
end
|
293
391
|
|
294
|
-
# Invoked via`after_update` callback for when a previous version is
|
392
|
+
# Invoked via`after_update` callback for when a previous version is
|
393
|
+
# reified and then saved.
|
295
394
|
def clear_version_instance!
|
296
395
|
send("#{self.class.version_association_name}=", nil)
|
297
396
|
end
|
298
397
|
|
398
|
+
# Invoked via callback when a user attempts to persist a reified
|
399
|
+
# `Version`.
|
299
400
|
def reset_timestamp_attrs_for_update_if_needed!
|
300
|
-
return if self.live?
|
301
|
-
timestamp_attributes_for_update_in_model.each
|
401
|
+
return if self.live?
|
402
|
+
timestamp_attributes_for_update_in_model.each do |column|
|
403
|
+
# ActiveRecord 4.2 deprecated `reset_column!` in favor of
|
404
|
+
# `restore_column!`.
|
405
|
+
if respond_to?("restore_#{column}!")
|
406
|
+
send("restore_#{column}!")
|
407
|
+
else
|
408
|
+
send("reset_#{column}!")
|
409
|
+
end
|
410
|
+
end
|
302
411
|
end
|
303
412
|
|
304
413
|
def record_destroy
|
305
414
|
if paper_trail_switched_on? and not new_record?
|
306
|
-
object_attrs = object_attrs_for_paper_trail(item_before_change)
|
307
415
|
data = {
|
308
416
|
:item_id => self.id,
|
309
417
|
:item_type => self.class.base_class.name,
|
310
418
|
:event => paper_trail_event || 'destroy',
|
311
|
-
:object =>
|
419
|
+
:object => pt_recordable_object,
|
312
420
|
:whodunnit => PaperTrail.whodunnit
|
313
421
|
}
|
314
|
-
|
422
|
+
if self.class.paper_trail_version_class.column_names.include?('transaction_id')
|
423
|
+
data[:transaction_id] = PaperTrail.transaction_id
|
424
|
+
end
|
425
|
+
version = self.class.paper_trail_version_class.create(merge_metadata(data))
|
426
|
+
send("#{self.class.version_association_name}=", version)
|
315
427
|
send(self.class.versions_association_name).send :load_target
|
428
|
+
set_transaction_id(version)
|
429
|
+
save_associations(version)
|
316
430
|
end
|
317
431
|
end
|
318
432
|
|
433
|
+
# Saves associations if the join table for `VersionAssociation` exists.
|
434
|
+
def save_associations(version)
|
435
|
+
return unless PaperTrail.config.track_associations?
|
436
|
+
self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
437
|
+
assoc_version_args = {
|
438
|
+
:version_id => version.id,
|
439
|
+
:foreign_key_name => assoc.foreign_key
|
440
|
+
}
|
441
|
+
|
442
|
+
if assoc.options[:polymorphic]
|
443
|
+
associated_record = send(assoc.name) if send(assoc.foreign_type)
|
444
|
+
if associated_record && associated_record.class.paper_trail_enabled_for_model?
|
445
|
+
assoc_version_args.merge!(:foreign_key_id => associated_record.id)
|
446
|
+
end
|
447
|
+
elsif assoc.klass.paper_trail_enabled_for_model?
|
448
|
+
assoc_version_args.merge!(:foreign_key_id => send(assoc.foreign_key))
|
449
|
+
end
|
450
|
+
|
451
|
+
PaperTrail::VersionAssociation.create(assoc_version_args) if assoc_version_args.has_key?(:foreign_key_id)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def set_transaction_id(version)
|
456
|
+
return unless self.class.paper_trail_version_class.column_names.include?('transaction_id')
|
457
|
+
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
|
458
|
+
PaperTrail.transaction_id = version.id
|
459
|
+
version.transaction_id = version.id
|
460
|
+
version.save
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def reset_transaction_id
|
465
|
+
PaperTrail.transaction_id = nil
|
466
|
+
end
|
467
|
+
|
319
468
|
def merge_metadata(data)
|
320
469
|
# First we merge the model-level metadata in `meta`.
|
321
470
|
paper_trail_options[:meta].each do |k,v|
|
@@ -323,8 +472,9 @@ module PaperTrail
|
|
323
472
|
if v.respond_to?(:call)
|
324
473
|
v.call(self)
|
325
474
|
elsif v.is_a?(Symbol) && respond_to?(v)
|
326
|
-
#
|
327
|
-
|
475
|
+
# If it is an attribute that is changing in an existing object,
|
476
|
+
# be sure to grab the current version.
|
477
|
+
if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] != 'create'
|
328
478
|
send("#{v}_was".to_sym)
|
329
479
|
else
|
330
480
|
send(v)
|
@@ -333,19 +483,14 @@ module PaperTrail
|
|
333
483
|
v
|
334
484
|
end
|
335
485
|
end
|
486
|
+
|
336
487
|
# Second we merge any extra data from the controller (if available).
|
337
488
|
data.merge(PaperTrail.controller_info || {})
|
338
489
|
end
|
339
490
|
|
340
|
-
def
|
341
|
-
|
342
|
-
|
343
|
-
all_timestamp_attributes.each do |column|
|
344
|
-
previous[column] = send(column) if self.class.column_names.include?(column.to_s) and not send(column).nil?
|
345
|
-
end
|
346
|
-
enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {}
|
347
|
-
previous.tap do |prev|
|
348
|
-
prev.id = id # `dup` clears the `id` so we add that back
|
491
|
+
def attributes_before_change
|
492
|
+
attributes.tap do |prev|
|
493
|
+
enums = self.respond_to?(:defined_enums) ? self.defined_enums : {}
|
349
494
|
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
|
350
495
|
before = enums[attr][before] if enums[attr]
|
351
496
|
prev[attr] = before
|
@@ -353,28 +498,38 @@ module PaperTrail
|
|
353
498
|
end
|
354
499
|
end
|
355
500
|
|
356
|
-
#
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
501
|
+
# Returns hash of attributes (with appropriate attributes serialized),
|
502
|
+
# ommitting attributes to be skipped.
|
503
|
+
def object_attrs_for_paper_trail(attributes_hash)
|
504
|
+
attrs = attributes_hash.except(*self.paper_trail_options[:skip])
|
505
|
+
self.class.serialize_attributes_for_paper_trail!(attrs)
|
506
|
+
attrs
|
361
507
|
end
|
362
508
|
|
363
|
-
#
|
364
|
-
#
|
365
|
-
#
|
366
|
-
# this by checking to ensure attributes other than those ignored and update timestamps are really being modified.
|
509
|
+
# Determines whether it is appropriate to generate a new version
|
510
|
+
# instance. A timestamp-only update (e.g. only `updated_at` changed) is
|
511
|
+
# considered notable unless an ignored attribute was also changed.
|
367
512
|
def changed_notably?
|
368
|
-
if
|
369
|
-
|
513
|
+
if ignored_attr_has_changed?
|
514
|
+
timestamps = timestamp_attributes_for_update_in_model.map(&:to_s)
|
515
|
+
(notably_changed - timestamps).any?
|
370
516
|
else
|
371
517
|
notably_changed.any?
|
372
518
|
end
|
373
519
|
end
|
374
520
|
|
521
|
+
# An attributed is "ignored" if it is listed in the `:ignore` option
|
522
|
+
# and/or the `:skip` option. Returns true if an ignored attribute has
|
523
|
+
# changed.
|
524
|
+
def ignored_attr_has_changed?
|
525
|
+
ignored = self.paper_trail_options[:ignore] + self.paper_trail_options[:skip]
|
526
|
+
ignored.any? && (changed & ignored).any?
|
527
|
+
end
|
528
|
+
|
375
529
|
def notably_changed
|
376
530
|
only = self.paper_trail_options[:only].dup
|
377
|
-
#
|
531
|
+
# Remove Hash arguments and then evaluate whether the attributes (the
|
532
|
+
# keys of the hash) should also get pushed into the collection.
|
378
533
|
only.delete_if do |obj|
|
379
534
|
obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
|
380
535
|
end
|
@@ -383,7 +538,8 @@ module PaperTrail
|
|
383
538
|
|
384
539
|
def changed_and_not_ignored
|
385
540
|
ignore = self.paper_trail_options[:ignore].dup
|
386
|
-
|
541
|
+
# Remove Hash arguments and then evaluate whether the attributes (the
|
542
|
+
# keys of the hash) should also get pushed into the collection.
|
387
543
|
ignore.delete_if do |obj|
|
388
544
|
obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
|
389
545
|
end
|