paper_trail 4.2.0 → 7.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/paper_trail/install_generator.rb +91 -17
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb.erb +12 -0
- data/lib/generators/paper_trail/templates/{add_transaction_id_column_to_versions.rb → add_transaction_id_column_to_versions.rb.erb} +3 -1
- data/lib/generators/paper_trail/templates/create_version_associations.rb.erb +22 -0
- data/lib/generators/paper_trail/templates/{create_versions.rb → create_versions.rb.erb} +9 -7
- data/lib/paper_trail.rb +180 -148
- data/lib/paper_trail/attribute_serializers/README.md +10 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +80 -0
- data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
- data/lib/paper_trail/cleaner.rb +16 -10
- data/lib/paper_trail/config.rb +28 -27
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
- data/lib/paper_trail/frameworks/cucumber.rb +1 -0
- data/lib/paper_trail/frameworks/rails.rb +2 -7
- data/lib/paper_trail/frameworks/rails/controller.rb +20 -18
- data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
- data/lib/paper_trail/frameworks/rspec.rb +17 -6
- data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
- data/lib/paper_trail/has_paper_trail.rb +25 -503
- data/lib/paper_trail/model_config.rb +207 -0
- data/lib/paper_trail/queries/versions/where_object.rb +60 -0
- data/lib/paper_trail/queries/versions/where_object_changes.rb +68 -0
- data/lib/paper_trail/record_history.rb +2 -12
- data/lib/paper_trail/record_trail.rb +573 -0
- data/lib/paper_trail/reifier.rb +164 -215
- data/lib/paper_trail/reifiers/belongs_to.rb +48 -0
- data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +50 -0
- data/lib/paper_trail/reifiers/has_many.rb +110 -0
- data/lib/paper_trail/reifiers/has_many_through.rb +90 -0
- data/lib/paper_trail/reifiers/has_one.rb +76 -0
- data/lib/paper_trail/serializers/json.rb +16 -7
- data/lib/paper_trail/serializers/yaml.rb +9 -13
- data/lib/paper_trail/version_association_concern.rb +3 -5
- data/lib/paper_trail/version_concern.rb +138 -111
- data/lib/paper_trail/version_number.rb +10 -9
- metadata +95 -327
- data/.gitignore +0 -22
- data/.rspec +0 -2
- data/.travis.yml +0 -41
- data/CHANGELOG.md +0 -362
- data/CONTRIBUTING.md +0 -84
- data/Gemfile +0 -2
- data/MIT-LICENSE +0 -20
- data/README.md +0 -1535
- data/Rakefile +0 -30
- data/doc/bug_report_template.rb +0 -65
- data/gemfiles/ar3.gemfile +0 -61
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +0 -10
- data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -17
- data/lib/paper_trail/attributes_serialization.rb +0 -89
- data/lib/paper_trail/frameworks/sinatra.rb +0 -38
- data/paper_trail.gemspec +0 -59
- data/spec/generators/install_generator_spec.rb +0 -67
- data/spec/models/animal_spec.rb +0 -36
- data/spec/models/boolit_spec.rb +0 -48
- data/spec/models/callback_modifier_spec.rb +0 -96
- data/spec/models/fluxor_spec.rb +0 -19
- data/spec/models/gadget_spec.rb +0 -70
- data/spec/models/joined_version_spec.rb +0 -47
- data/spec/models/json_version_spec.rb +0 -103
- data/spec/models/kitchen/banana_spec.rb +0 -14
- data/spec/models/not_on_update_spec.rb +0 -19
- data/spec/models/post_with_status_spec.rb +0 -17
- data/spec/models/skipper_spec.rb +0 -46
- data/spec/models/thing_spec.rb +0 -11
- data/spec/models/version_spec.rb +0 -239
- data/spec/models/widget_spec.rb +0 -298
- data/spec/modules/paper_trail_spec.rb +0 -27
- data/spec/modules/version_concern_spec.rb +0 -32
- data/spec/modules/version_number_spec.rb +0 -44
- data/spec/paper_trail/config_spec.rb +0 -52
- data/spec/paper_trail_spec.rb +0 -66
- data/spec/rails_helper.rb +0 -34
- data/spec/requests/articles_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -95
- data/spec/support/alt_db_init.rb +0 -59
- data/test/custom_json_serializer.rb +0 -13
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -20
- data/test/dummy/app/controllers/articles_controller.rb +0 -17
- data/test/dummy/app/controllers/test_controller.rb +0 -5
- data/test/dummy/app/controllers/widgets_controller.rb +0 -31
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/models/animal.rb +0 -6
- data/test/dummy/app/models/article.rb +0 -16
- data/test/dummy/app/models/authorship.rb +0 -5
- data/test/dummy/app/models/book.rb +0 -9
- data/test/dummy/app/models/boolit.rb +0 -4
- data/test/dummy/app/models/callback_modifier.rb +0 -45
- data/test/dummy/app/models/cat.rb +0 -2
- data/test/dummy/app/models/chapter.rb +0 -9
- data/test/dummy/app/models/citation.rb +0 -5
- data/test/dummy/app/models/customer.rb +0 -4
- data/test/dummy/app/models/document.rb +0 -4
- data/test/dummy/app/models/dog.rb +0 -2
- data/test/dummy/app/models/editor.rb +0 -4
- data/test/dummy/app/models/editorship.rb +0 -5
- data/test/dummy/app/models/elephant.rb +0 -3
- data/test/dummy/app/models/fluxor.rb +0 -3
- data/test/dummy/app/models/foo_widget.rb +0 -2
- data/test/dummy/app/models/fruit.rb +0 -5
- data/test/dummy/app/models/gadget.rb +0 -3
- data/test/dummy/app/models/kitchen/banana.rb +0 -5
- data/test/dummy/app/models/legacy_widget.rb +0 -4
- data/test/dummy/app/models/line_item.rb +0 -4
- data/test/dummy/app/models/not_on_update.rb +0 -4
- data/test/dummy/app/models/order.rb +0 -5
- data/test/dummy/app/models/paragraph.rb +0 -5
- data/test/dummy/app/models/person.rb +0 -38
- data/test/dummy/app/models/post.rb +0 -3
- data/test/dummy/app/models/post_with_status.rb +0 -8
- data/test/dummy/app/models/protected_widget.rb +0 -3
- data/test/dummy/app/models/quotation.rb +0 -5
- data/test/dummy/app/models/section.rb +0 -6
- data/test/dummy/app/models/skipper.rb +0 -6
- data/test/dummy/app/models/song.rb +0 -32
- data/test/dummy/app/models/thing.rb +0 -3
- data/test/dummy/app/models/translation.rb +0 -4
- data/test/dummy/app/models/whatchamajigger.rb +0 -4
- data/test/dummy/app/models/widget.rb +0 -15
- data/test/dummy/app/models/wotsit.rb +0 -8
- data/test/dummy/app/versions/joined_version.rb +0 -5
- data/test/dummy/app/versions/json_version.rb +0 -3
- data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
- data/test/dummy/app/versions/post_version.rb +0 -3
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -69
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.mysql.yml +0 -19
- data/test/dummy/config/database.postgres.yml +0 -15
- data/test/dummy/config/database.sqlite.yml +0 -15
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -40
- data/test/dummy/config/environments/production.rb +0 -73
- data/test/dummy/config/environments/test.rb +0 -41
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/paper_trail.rb +0 -10
- data/test/dummy/config/initializers/secret_token.rb +0 -7
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -4
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -287
- data/test/dummy/db/schema.rb +0 -246
- data/test/dummy/script/rails +0 -6
- data/test/functional/controller_test.rb +0 -91
- data/test/functional/enabled_for_controller_test.rb +0 -29
- data/test/functional/modular_sinatra_test.rb +0 -48
- data/test/functional/sinatra_test.rb +0 -49
- data/test/functional/thread_safety_test.rb +0 -48
- data/test/paper_trail_test.rb +0 -38
- data/test/test_helper.rb +0 -105
- data/test/time_travel_helper.rb +0 -15
- data/test/unit/associations_test.rb +0 -726
- data/test/unit/cleaner_test.rb +0 -182
- data/test/unit/inheritance_column_test.rb +0 -43
- data/test/unit/model_test.rb +0 -1373
- data/test/unit/protected_attrs_test.rb +0 -47
- data/test/unit/serializer_test.rb +0 -117
- data/test/unit/serializers/json_test.rb +0 -88
- data/test/unit/serializers/mixin_json_test.rb +0 -36
- data/test/unit/serializers/mixin_yaml_test.rb +0 -49
- data/test/unit/serializers/yaml_test.rb +0 -52
- data/test/unit/timestamp_test.rb +0 -43
- data/test/unit/version_test.rb +0 -119
@@ -1,7 +1,12 @@
|
|
1
1
|
module PaperTrail
|
2
2
|
module Rails
|
3
|
+
# See http://guides.rubyonrails.org/engines.html
|
3
4
|
class Engine < ::Rails::Engine
|
4
|
-
paths[
|
5
|
+
paths["app/models"] << "lib/paper_trail/frameworks/active_record/models"
|
6
|
+
config.paper_trail = ActiveSupport::OrderedOptions.new
|
7
|
+
initializer "paper_trail.initialisation" do |app|
|
8
|
+
PaperTrail.enabled = app.config.paper_trail.fetch(:enabled, true)
|
9
|
+
end
|
5
10
|
end
|
6
11
|
end
|
7
12
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "rspec/core"
|
2
|
+
require "rspec/matchers"
|
3
|
+
require "paper_trail/frameworks/rspec/helpers"
|
4
4
|
|
5
5
|
RSpec.configure do |config|
|
6
6
|
config.include ::PaperTrail::RSpec::Helpers::InstanceMethods
|
@@ -13,17 +13,28 @@ RSpec.configure do |config|
|
|
13
13
|
::PaperTrail.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
|
14
14
|
end
|
15
15
|
|
16
|
-
config.before(:each, :
|
16
|
+
config.before(:each, versioning: true) do
|
17
17
|
::PaperTrail.enabled = true
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
RSpec::Matchers.define :be_versioned do
|
22
22
|
# check to see if the model has `has_paper_trail` declared on it
|
23
|
-
match { |actual| actual.
|
23
|
+
match { |actual| actual.is_a?(::PaperTrail::Model::InstanceMethods) }
|
24
24
|
end
|
25
25
|
|
26
26
|
RSpec::Matchers.define :have_a_version_with do |attributes|
|
27
27
|
# check if the model has a version with the specified attributes
|
28
|
-
match
|
28
|
+
match do |actual|
|
29
|
+
versions_association = actual.class.versions_association_name
|
30
|
+
actual.send(versions_association).where_object(attributes).any?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Matchers.define :have_a_version_with_changes do |attributes|
|
35
|
+
# check if the model has a version changes with the specified attributes
|
36
|
+
match do |actual|
|
37
|
+
versions_association = actual.class.versions_association_name
|
38
|
+
actual.send(versions_association).where_object_changes(attributes).any?
|
39
|
+
end
|
29
40
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module PaperTrail
|
2
2
|
module RSpec
|
3
3
|
module Helpers
|
4
|
+
# Included in the RSpec configuration in `frameworks/rspec.rb`
|
4
5
|
module InstanceMethods
|
5
6
|
# enable versioning for specific blocks (at instance-level)
|
6
7
|
def with_versioning
|
@@ -12,10 +13,11 @@ module PaperTrail
|
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
16
|
+
# Extended by the RSpec configuration in `frameworks/rspec.rb`
|
15
17
|
module ClassMethods
|
16
18
|
# enable versioning for specific blocks (at class-level)
|
17
19
|
def with_versioning(&block)
|
18
|
-
context
|
20
|
+
context "with versioning", versioning: true do
|
19
21
|
class_exec(&block)
|
20
22
|
end
|
21
23
|
end
|
@@ -1,13 +1,21 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "active_support/core_ext/object" # provides the `try` method
|
2
|
+
require "paper_trail/attribute_serializers/legacy_active_record_shim"
|
3
|
+
require "paper_trail/attribute_serializers/object_attribute"
|
4
|
+
require "paper_trail/attribute_serializers/object_changes_attribute"
|
5
|
+
require "paper_trail/model_config"
|
6
|
+
require "paper_trail/record_trail"
|
3
7
|
|
4
8
|
module PaperTrail
|
9
|
+
# Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
|
10
|
+
# It is our goal to have the smallest possible footprint here, because
|
11
|
+
# `ActiveRecord::Base` is a very crowded namespace! That is why we introduced
|
12
|
+
# `.paper_trail` and `#paper_trail`.
|
5
13
|
module Model
|
6
|
-
|
7
14
|
def self.included(base)
|
8
15
|
base.send :extend, ClassMethods
|
9
16
|
end
|
10
17
|
|
18
|
+
# :nodoc:
|
11
19
|
module ClassMethods
|
12
20
|
# Declare this in your model to track every create, update, and destroy.
|
13
21
|
# Each version of the model is available in the `versions` association.
|
@@ -45,516 +53,30 @@ module PaperTrail
|
|
45
53
|
# the instance was reified from. Default is `:version`.
|
46
54
|
# - :save_changes - Whether or not to save changes to the object_changes
|
47
55
|
# column if it exists. Default is true
|
56
|
+
# - :join_tables - If the model has a has_and_belongs_to_many relation
|
57
|
+
# with an unpapertrailed model, passing the name of the association to
|
58
|
+
# the join_tables option will paper trail the join table but not save
|
59
|
+
# the other model, allowing reification of the association but with the
|
60
|
+
# other models latest state (if the other model is paper trailed, this
|
61
|
+
# option does nothing)
|
48
62
|
#
|
63
|
+
# @api public
|
49
64
|
def has_paper_trail(options = {})
|
50
|
-
options
|
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]
|
65
|
+
paper_trail.setup(options)
|
59
66
|
end
|
60
67
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
send :include, InstanceMethods
|
65
|
-
send :extend, AttributesSerialization
|
66
|
-
|
67
|
-
class_attribute :version_association_name
|
68
|
-
self.version_association_name = options[:version] || :version
|
69
|
-
|
70
|
-
# The version this instance was reified from.
|
71
|
-
attr_accessor self.version_association_name
|
72
|
-
|
73
|
-
class_attribute :version_class_name
|
74
|
-
self.version_class_name = options[:class_name] || 'PaperTrail::Version'
|
75
|
-
|
76
|
-
class_attribute :paper_trail_options
|
77
|
-
|
78
|
-
self.paper_trail_options = options.dup
|
79
|
-
|
80
|
-
[:ignore, :skip, :only].each do |k|
|
81
|
-
paper_trail_options[k] =
|
82
|
-
[paper_trail_options[k]].flatten.compact.map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
|
83
|
-
end
|
84
|
-
|
85
|
-
paper_trail_options[:meta] ||= {}
|
86
|
-
paper_trail_options[:save_changes] = true if paper_trail_options[:save_changes].nil?
|
87
|
-
|
88
|
-
class_attribute :versions_association_name
|
89
|
-
self.versions_association_name = options[:versions] || :versions
|
90
|
-
|
91
|
-
attr_accessor :paper_trail_event
|
92
|
-
|
93
|
-
# `has_many` syntax for specifying order uses a lambda in Rails 4
|
94
|
-
if ::ActiveRecord::VERSION::MAJOR >= 4
|
95
|
-
has_many self.versions_association_name,
|
96
|
-
lambda { order(model.timestamp_sort_order) },
|
97
|
-
:class_name => self.version_class_name, :as => :item
|
98
|
-
else
|
99
|
-
has_many self.versions_association_name,
|
100
|
-
:class_name => self.version_class_name,
|
101
|
-
:as => :item,
|
102
|
-
:order => self.paper_trail_version_class.timestamp_sort_order
|
103
|
-
end
|
104
|
-
|
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
|
109
|
-
end
|
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
|
-
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
|
133
|
-
|
134
|
-
send "#{recording_order}_destroy", :record_destroy, :if => :save_version?
|
135
|
-
|
136
|
-
return if paper_trail_options[:on].include?(:destroy)
|
137
|
-
paper_trail_options[:on] << :destroy
|
138
|
-
end
|
139
|
-
|
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!
|
147
|
-
|
148
|
-
return if paper_trail_options[:on].include?(:update)
|
149
|
-
paper_trail_options[:on] << :update
|
150
|
-
end
|
151
|
-
|
152
|
-
# Record version after "create" event
|
153
|
-
def paper_trail_on_create
|
154
|
-
after_create :record_create,
|
155
|
-
:if => :save_version?
|
156
|
-
|
157
|
-
return if paper_trail_options[:on].include?(:create)
|
158
|
-
paper_trail_options[:on] << :create
|
159
|
-
end
|
160
|
-
|
161
|
-
# Switches PaperTrail off for this class.
|
162
|
-
def paper_trail_off!
|
163
|
-
PaperTrail.enabled_for_model(self, false)
|
164
|
-
end
|
165
|
-
|
166
|
-
# Switches PaperTrail on for this class.
|
167
|
-
def paper_trail_on!
|
168
|
-
PaperTrail.enabled_for_model(self, true)
|
169
|
-
end
|
170
|
-
|
171
|
-
def paper_trail_enabled_for_model?
|
172
|
-
return false unless self.include?(PaperTrail::Model::InstanceMethods)
|
173
|
-
PaperTrail.enabled_for_model?(self)
|
174
|
-
end
|
175
|
-
|
176
|
-
def paper_trail_version_class
|
177
|
-
@paper_trail_version_class ||= version_class_name.constantize
|
68
|
+
# @api public
|
69
|
+
def paper_trail
|
70
|
+
::PaperTrail::ModelConfig.new(self)
|
178
71
|
end
|
179
72
|
end
|
180
73
|
|
181
74
|
# Wrap the following methods in a module so we can include them only in the
|
182
75
|
# ActiveRecord models that declare `has_paper_trail`.
|
183
76
|
module InstanceMethods
|
184
|
-
#
|
185
|
-
|
186
|
-
|
187
|
-
source_version.nil?
|
188
|
-
end
|
189
|
-
|
190
|
-
# Returns who put the object into its current state.
|
191
|
-
def paper_trail_originator
|
192
|
-
(source_version || send(self.class.versions_association_name).last).try(:whodunnit)
|
193
|
-
end
|
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
|
-
|
206
|
-
# Returns the object (not a Version) as it was at the given timestamp.
|
207
|
-
def version_at(timestamp, reify_options={})
|
208
|
-
# Because a version stores how its object looked *before* the change,
|
209
|
-
# we need to look for the first version created *after* the timestamp.
|
210
|
-
v = send(self.class.versions_association_name).subsequent(timestamp, true).first
|
211
|
-
return v.reify(reify_options) if v
|
212
|
-
self unless self.destroyed?
|
213
|
-
end
|
214
|
-
|
215
|
-
# Returns the objects (not Versions) as they were between the given times.
|
216
|
-
def versions_between(start_time, end_time, reify_options={})
|
217
|
-
versions = send(self.class.versions_association_name).between(start_time, end_time)
|
218
|
-
versions.collect { |version| version_at(version.send PaperTrail.timestamp_field) }
|
219
|
-
end
|
220
|
-
|
221
|
-
# Returns the object (not a Version) as it was most recently.
|
222
|
-
def previous_version
|
223
|
-
preceding_version = source_version ? source_version.previous : send(self.class.versions_association_name).last
|
224
|
-
preceding_version.reify if preceding_version
|
225
|
-
end
|
226
|
-
|
227
|
-
# Returns the object (not a Version) as it became next.
|
228
|
-
# NOTE: if self (the item) was not reified from a version, i.e. it is the
|
229
|
-
# "live" item, we return nil. Perhaps we should return self instead?
|
230
|
-
def next_version
|
231
|
-
subsequent_version = source_version.next
|
232
|
-
subsequent_version ? subsequent_version.reify : self.class.find(self.id)
|
233
|
-
rescue
|
234
|
-
nil
|
235
|
-
end
|
236
|
-
|
237
|
-
def paper_trail_enabled_for_model?
|
238
|
-
self.class.paper_trail_enabled_for_model?
|
239
|
-
end
|
240
|
-
|
241
|
-
# Executes the given method or block without creating a new version.
|
242
|
-
def without_versioning(method = nil)
|
243
|
-
paper_trail_was_enabled = self.paper_trail_enabled_for_model?
|
244
|
-
self.class.paper_trail_off!
|
245
|
-
method ? method.to_proc.call(self) : yield(self)
|
246
|
-
ensure
|
247
|
-
self.class.paper_trail_on! if paper_trail_was_enabled
|
248
|
-
end
|
249
|
-
|
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.
|
263
|
-
def whodunnit(value)
|
264
|
-
raise ArgumentError, 'expected to receive a block' unless block_given?
|
265
|
-
current_whodunnit = PaperTrail.whodunnit
|
266
|
-
PaperTrail.whodunnit = value
|
267
|
-
yield self
|
268
|
-
ensure
|
269
|
-
PaperTrail.whodunnit = current_whodunnit
|
270
|
-
end
|
271
|
-
|
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`.
|
275
|
-
#
|
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)
|
280
|
-
def touch_with_version(name = nil)
|
281
|
-
raise ActiveRecordError, "can not touch on a new record object" unless persisted?
|
282
|
-
|
283
|
-
attributes = timestamp_attributes_for_update_in_model
|
284
|
-
attributes << name if name
|
285
|
-
current_time = current_time_from_proper_timezone
|
286
|
-
|
287
|
-
attributes.each { |column| write_attribute(column, current_time) }
|
288
|
-
|
289
|
-
record_update(true) unless will_record_after_update?
|
290
|
-
save!(:validate => false)
|
291
|
-
end
|
292
|
-
|
293
|
-
private
|
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
|
-
|
302
|
-
def source_version
|
303
|
-
send self.class.version_association_name
|
304
|
-
end
|
305
|
-
|
306
|
-
def record_create
|
307
|
-
if paper_trail_switched_on?
|
308
|
-
data = {
|
309
|
-
:event => paper_trail_event || 'create',
|
310
|
-
:whodunnit => PaperTrail.whodunnit
|
311
|
-
}
|
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
|
320
|
-
end
|
321
|
-
version = send(self.class.versions_association_name).create! merge_metadata(data)
|
322
|
-
set_transaction_id(version)
|
323
|
-
save_associations(version)
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
def record_update(force = nil)
|
328
|
-
if paper_trail_switched_on? && (force || changed_notably?)
|
329
|
-
data = {
|
330
|
-
:event => paper_trail_event || 'update',
|
331
|
-
:object => pt_recordable_object,
|
332
|
-
:whodunnit => PaperTrail.whodunnit
|
333
|
-
}
|
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
|
342
|
-
end
|
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)
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
def changes_for_paper_trail
|
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
|
390
|
-
end
|
391
|
-
|
392
|
-
# Invoked via`after_update` callback for when a previous version is
|
393
|
-
# reified and then saved.
|
394
|
-
def clear_version_instance!
|
395
|
-
send("#{self.class.version_association_name}=", nil)
|
396
|
-
end
|
397
|
-
|
398
|
-
# Invoked via callback when a user attempts to persist a reified
|
399
|
-
# `Version`.
|
400
|
-
def reset_timestamp_attrs_for_update_if_needed!
|
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
|
411
|
-
end
|
412
|
-
|
413
|
-
def record_destroy
|
414
|
-
if paper_trail_switched_on? and not new_record?
|
415
|
-
data = {
|
416
|
-
:item_id => self.id,
|
417
|
-
:item_type => self.class.base_class.name,
|
418
|
-
:event => paper_trail_event || 'destroy',
|
419
|
-
:object => pt_recordable_object,
|
420
|
-
:whodunnit => PaperTrail.whodunnit
|
421
|
-
}
|
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)
|
427
|
-
send(self.class.versions_association_name).send :load_target
|
428
|
-
set_transaction_id(version)
|
429
|
-
save_associations(version)
|
430
|
-
end
|
431
|
-
end
|
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
|
-
|
468
|
-
def merge_metadata(data)
|
469
|
-
# First we merge the model-level metadata in `meta`.
|
470
|
-
paper_trail_options[:meta].each do |k,v|
|
471
|
-
data[k] =
|
472
|
-
if v.respond_to?(:call)
|
473
|
-
v.call(self)
|
474
|
-
elsif v.is_a?(Symbol) && respond_to?(v)
|
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'
|
478
|
-
send("#{v}_was".to_sym)
|
479
|
-
else
|
480
|
-
send(v)
|
481
|
-
end
|
482
|
-
else
|
483
|
-
v
|
484
|
-
end
|
485
|
-
end
|
486
|
-
|
487
|
-
# Second we merge any extra data from the controller (if available).
|
488
|
-
data.merge(PaperTrail.controller_info || {})
|
489
|
-
end
|
490
|
-
|
491
|
-
def attributes_before_change
|
492
|
-
attributes.tap do |prev|
|
493
|
-
enums = self.respond_to?(:defined_enums) ? self.defined_enums : {}
|
494
|
-
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
|
495
|
-
before = enums[attr][before] if enums[attr]
|
496
|
-
prev[attr] = before
|
497
|
-
end
|
498
|
-
end
|
499
|
-
end
|
500
|
-
|
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
|
507
|
-
end
|
508
|
-
|
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.
|
512
|
-
def changed_notably?
|
513
|
-
if ignored_attr_has_changed?
|
514
|
-
timestamps = timestamp_attributes_for_update_in_model.map(&:to_s)
|
515
|
-
(notably_changed - timestamps).any?
|
516
|
-
else
|
517
|
-
notably_changed.any?
|
518
|
-
end
|
519
|
-
end
|
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
|
-
|
529
|
-
def notably_changed
|
530
|
-
only = self.paper_trail_options[:only].dup
|
531
|
-
# Remove Hash arguments and then evaluate whether the attributes (the
|
532
|
-
# keys of the hash) should also get pushed into the collection.
|
533
|
-
only.delete_if do |obj|
|
534
|
-
obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
|
535
|
-
end
|
536
|
-
only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
|
537
|
-
end
|
538
|
-
|
539
|
-
def changed_and_not_ignored
|
540
|
-
ignore = self.paper_trail_options[:ignore].dup
|
541
|
-
# Remove Hash arguments and then evaluate whether the attributes (the
|
542
|
-
# keys of the hash) should also get pushed into the collection.
|
543
|
-
ignore.delete_if do |obj|
|
544
|
-
obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
|
545
|
-
end
|
546
|
-
skip = self.paper_trail_options[:skip]
|
547
|
-
changed - ignore - skip
|
548
|
-
end
|
549
|
-
|
550
|
-
def paper_trail_switched_on?
|
551
|
-
PaperTrail.enabled? && PaperTrail.enabled_for_controller? && self.paper_trail_enabled_for_model?
|
552
|
-
end
|
553
|
-
|
554
|
-
def save_version?
|
555
|
-
if_condition = self.paper_trail_options[:if]
|
556
|
-
unless_condition = self.paper_trail_options[:unless]
|
557
|
-
(if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
|
77
|
+
# @api public
|
78
|
+
def paper_trail
|
79
|
+
::PaperTrail::RecordTrail.new(self)
|
558
80
|
end
|
559
81
|
end
|
560
82
|
end
|