paper_trail 4.2.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +28 -9
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +100 -0
- data/.rubocop_todo.yml +14 -0
- data/.travis.yml +8 -9
- data/Appraisals +41 -0
- data/CHANGELOG.md +49 -9
- data/Gemfile +1 -1
- data/README.md +130 -109
- data/Rakefile +19 -19
- data/doc/bug_report_template.rb +20 -14
- data/gemfiles/ar3.gemfile +10 -53
- data/gemfiles/ar4.gemfile +7 -0
- data/gemfiles/ar5.gemfile +13 -0
- data/lib/generators/paper_trail/install_generator.rb +26 -18
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +4 -2
- data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
- data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
- data/lib/generators/paper_trail/templates/create_versions.rb +39 -5
- data/lib/paper_trail.rb +169 -146
- data/lib/paper_trail/attributes_serialization.rb +89 -17
- data/lib/paper_trail/cleaner.rb +15 -9
- data/lib/paper_trail/config.rb +28 -11
- data/lib/paper_trail/frameworks/active_record.rb +4 -0
- 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 +29 -9
- data/lib/paper_trail/frameworks/rails/engine.rb +7 -1
- data/lib/paper_trail/frameworks/rspec.rb +5 -5
- data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
- data/lib/paper_trail/frameworks/sinatra.rb +6 -4
- data/lib/paper_trail/has_paper_trail.rb +199 -106
- data/lib/paper_trail/record_history.rb +1 -3
- data/lib/paper_trail/reifier.rb +297 -118
- data/lib/paper_trail/serializers/json.rb +3 -3
- data/lib/paper_trail/serializers/yaml.rb +27 -8
- data/lib/paper_trail/version_association_concern.rb +3 -1
- data/lib/paper_trail/version_concern.rb +75 -35
- data/lib/paper_trail/version_number.rb +6 -9
- data/paper_trail.gemspec +44 -51
- data/spec/generators/install_generator_spec.rb +24 -25
- data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
- data/spec/models/animal_spec.rb +12 -12
- data/spec/models/boolit_spec.rb +8 -8
- data/spec/models/callback_modifier_spec.rb +47 -47
- data/spec/models/car_spec.rb +13 -0
- data/spec/models/fluxor_spec.rb +3 -3
- data/spec/models/gadget_spec.rb +19 -19
- data/spec/models/joined_version_spec.rb +3 -3
- data/spec/models/json_version_spec.rb +23 -24
- data/spec/models/kitchen/banana_spec.rb +3 -3
- data/spec/models/not_on_update_spec.rb +7 -4
- data/spec/models/post_with_status_spec.rb +13 -3
- data/spec/models/skipper_spec.rb +10 -10
- data/spec/models/thing_spec.rb +4 -4
- data/spec/models/truck_spec.rb +5 -0
- data/spec/models/vehicle_spec.rb +5 -0
- data/spec/models/version_spec.rb +103 -59
- data/spec/models/widget_spec.rb +82 -52
- data/spec/modules/paper_trail_spec.rb +2 -2
- data/spec/modules/version_concern_spec.rb +11 -12
- data/spec/modules/version_number_spec.rb +2 -4
- data/spec/paper_trail/config_spec.rb +10 -29
- data/spec/paper_trail_spec.rb +16 -14
- data/spec/rails_helper.rb +10 -9
- data/spec/requests/articles_spec.rb +11 -7
- data/spec/spec_helper.rb +41 -22
- data/spec/support/alt_db_init.rb +8 -13
- data/test/custom_json_serializer.rb +3 -3
- data/test/dummy/Rakefile +2 -2
- data/test/dummy/app/controllers/application_controller.rb +21 -8
- data/test/dummy/app/controllers/articles_controller.rb +11 -8
- data/test/dummy/app/controllers/widgets_controller.rb +13 -12
- data/test/dummy/app/models/animal.rb +1 -1
- data/test/dummy/app/models/article.rb +19 -11
- data/test/dummy/app/models/authorship.rb +1 -1
- data/test/dummy/app/models/bar_habtm.rb +4 -0
- data/test/dummy/app/models/book.rb +4 -4
- data/test/dummy/app/models/boolit.rb +1 -1
- data/test/dummy/app/models/callback_modifier.rb +6 -6
- data/test/dummy/app/models/car.rb +3 -0
- data/test/dummy/app/models/chapter.rb +4 -4
- data/test/dummy/app/models/customer.rb +1 -1
- data/test/dummy/app/models/document.rb +2 -2
- data/test/dummy/app/models/editor.rb +1 -1
- data/test/dummy/app/models/foo_habtm.rb +4 -0
- data/test/dummy/app/models/fruit.rb +2 -2
- data/test/dummy/app/models/gadget.rb +1 -1
- data/test/dummy/app/models/kitchen/banana.rb +1 -1
- data/test/dummy/app/models/legacy_widget.rb +2 -2
- data/test/dummy/app/models/line_item.rb +1 -1
- data/test/dummy/app/models/not_on_update.rb +1 -1
- data/test/dummy/app/models/person.rb +6 -6
- data/test/dummy/app/models/post.rb +1 -1
- data/test/dummy/app/models/post_with_status.rb +1 -1
- data/test/dummy/app/models/quotation.rb +1 -1
- data/test/dummy/app/models/section.rb +1 -1
- data/test/dummy/app/models/skipper.rb +2 -2
- data/test/dummy/app/models/song.rb +13 -4
- data/test/dummy/app/models/thing.rb +2 -2
- data/test/dummy/app/models/translation.rb +2 -2
- data/test/dummy/app/models/truck.rb +4 -0
- data/test/dummy/app/models/vehicle.rb +4 -0
- data/test/dummy/app/models/whatchamajigger.rb +1 -1
- data/test/dummy/app/models/widget.rb +7 -6
- data/test/dummy/app/versions/joined_version.rb +4 -3
- data/test/dummy/app/versions/json_version.rb +1 -1
- data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
- data/test/dummy/app/versions/post_version.rb +2 -2
- data/test/dummy/config.ru +1 -1
- data/test/dummy/config/application.rb +20 -9
- data/test/dummy/config/boot.rb +5 -5
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/development.rb +4 -3
- data/test/dummy/config/environments/production.rb +3 -2
- data/test/dummy/config/environments/test.rb +15 -5
- data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
- data/test/dummy/config/initializers/paper_trail.rb +1 -2
- data/test/dummy/config/initializers/secret_token.rb +3 -1
- data/test/dummy/config/initializers/session_store.rb +1 -1
- data/test/dummy/config/routes.rb +2 -2
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -74
- data/test/dummy/db/schema.rb +29 -6
- data/test/dummy/script/rails +6 -4
- data/test/functional/controller_test.rb +34 -35
- data/test/functional/enabled_for_controller_test.rb +6 -7
- data/test/functional/modular_sinatra_test.rb +43 -38
- data/test/functional/sinatra_test.rb +49 -40
- data/test/functional/thread_safety_test.rb +4 -6
- data/test/paper_trail_test.rb +15 -14
- data/test/test_helper.rb +68 -44
- data/test/time_travel_helper.rb +1 -15
- data/test/unit/associations_test.rb +517 -251
- data/test/unit/cleaner_test.rb +66 -60
- data/test/unit/inheritance_column_test.rb +17 -17
- data/test/unit/model_test.rb +611 -504
- data/test/unit/protected_attrs_test.rb +16 -12
- data/test/unit/serializer_test.rb +44 -43
- data/test/unit/serializers/json_test.rb +17 -18
- data/test/unit/serializers/mixin_json_test.rb +15 -14
- data/test/unit/serializers/mixin_yaml_test.rb +20 -16
- data/test/unit/serializers/yaml_test.rb +12 -13
- data/test/unit/timestamp_test.rb +10 -12
- data/test/unit/version_test.rb +7 -7
- metadata +92 -40
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
1
|
+
require "active_support/core_ext/object" # provides the `try` method
|
2
2
|
|
3
3
|
module PaperTrail
|
4
|
+
# Extensions to `Sinatra`.
|
4
5
|
module Sinatra
|
5
|
-
|
6
6
|
# Register this module inside your Sinatra application to gain access to
|
7
7
|
# controller-level methods used by PaperTrail.
|
8
8
|
def self.registered(app)
|
@@ -29,10 +29,12 @@ module PaperTrail
|
|
29
29
|
|
30
30
|
# Tells PaperTrail who is responsible for any changes that occur.
|
31
31
|
def set_paper_trail_whodunnit
|
32
|
+
@set_paper_trail_whodunnit_called = true
|
32
33
|
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled?
|
33
34
|
end
|
34
|
-
|
35
35
|
end
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
+
if defined?(::Sinatra)
|
39
|
+
::Sinatra.register(::PaperTrail::Sinatra)
|
38
40
|
end
|
@@ -1,13 +1,15 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "active_support/core_ext/object" # provides the `try` method
|
2
|
+
require "paper_trail/attributes_serialization"
|
3
3
|
|
4
4
|
module PaperTrail
|
5
|
+
# Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
|
5
6
|
module Model
|
6
|
-
|
7
7
|
def self.included(base)
|
8
8
|
base.send :extend, ClassMethods
|
9
|
+
base.send :attr_accessor, :paper_trail_habtm
|
9
10
|
end
|
10
11
|
|
12
|
+
# :nodoc:
|
11
13
|
module ClassMethods
|
12
14
|
# Declare this in your model to track every create, update, and destroy.
|
13
15
|
# Each version of the model is available in the `versions` association.
|
@@ -45,7 +47,14 @@ module PaperTrail
|
|
45
47
|
# the instance was reified from. Default is `:version`.
|
46
48
|
# - :save_changes - Whether or not to save changes to the object_changes
|
47
49
|
# column if it exists. Default is true
|
50
|
+
# - :join_tables - If the model has a has_and_belongs_to_many relation
|
51
|
+
# with an unpapertrailed model, passing the name of the association to
|
52
|
+
# the join_tables option will paper trail the join table but not save
|
53
|
+
# the other model, allowing reification of the association but with the
|
54
|
+
# other models latest state (if the other model is paper trailed, this
|
55
|
+
# option does nothing)
|
48
56
|
#
|
57
|
+
# @api public
|
49
58
|
def has_paper_trail(options = {})
|
50
59
|
options[:on] ||= [:create, :update, :destroy]
|
51
60
|
|
@@ -56,8 +65,47 @@ module PaperTrail
|
|
56
65
|
setup_model_for_paper_trail(options)
|
57
66
|
|
58
67
|
setup_callbacks_from_options options[:on]
|
68
|
+
|
69
|
+
setup_callbacks_for_habtm options[:join_tables]
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_for_callback(name, callback, model, assoc)
|
73
|
+
model.paper_trail_habtm ||= {}
|
74
|
+
model.paper_trail_habtm.reverse_merge!(name => { removed: [], added: [] })
|
75
|
+
case callback
|
76
|
+
when :before_add
|
77
|
+
model.paper_trail_habtm[name][:added] |= [assoc.id]
|
78
|
+
model.paper_trail_habtm[name][:removed] -= [assoc.id]
|
79
|
+
when :before_remove
|
80
|
+
model.paper_trail_habtm[name][:removed] |= [assoc.id]
|
81
|
+
model.paper_trail_habtm[name][:added] -= [assoc.id]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
attr_reader :paper_trail_save_join_tables
|
86
|
+
|
87
|
+
def setup_callbacks_for_habtm(join_tables)
|
88
|
+
@paper_trail_save_join_tables = Array.wrap(join_tables)
|
89
|
+
# Adds callbacks to record changes to habtm associations such that on
|
90
|
+
# save the previous version of the association (if changed) can be
|
91
|
+
# interpreted
|
92
|
+
reflect_on_all_associations(:has_and_belongs_to_many).
|
93
|
+
reject { |a| paper_trail_options[:skip].include?(a.name.to_s) }.
|
94
|
+
each do |a|
|
95
|
+
added_callback = lambda do |*args|
|
96
|
+
update_for_callback(a.name, :before_add, args[-2], args.last)
|
97
|
+
end
|
98
|
+
removed_callback = lambda do |*args|
|
99
|
+
update_for_callback(a.name, :before_remove, args[-2], args.last)
|
100
|
+
end
|
101
|
+
send(:"before_add_for_#{a.name}").send(:<<, added_callback)
|
102
|
+
send(:"before_remove_for_#{a.name}").send(:<<, removed_callback)
|
103
|
+
end
|
59
104
|
end
|
60
105
|
|
106
|
+
# Installs callbacks, associations, "class attributes", and more.
|
107
|
+
# For details of how "class attributes" work, see the activesupport docs.
|
108
|
+
# @api private
|
61
109
|
def setup_model_for_paper_trail(options = {})
|
62
110
|
# Lazily include the instance methods so we don't clutter up
|
63
111
|
# any more ActiveRecord models than we have to.
|
@@ -68,22 +116,12 @@ module PaperTrail
|
|
68
116
|
self.version_association_name = options[:version] || :version
|
69
117
|
|
70
118
|
# The version this instance was reified from.
|
71
|
-
attr_accessor
|
119
|
+
attr_accessor version_association_name
|
72
120
|
|
73
121
|
class_attribute :version_class_name
|
74
|
-
self.version_class_name = options[:class_name] ||
|
122
|
+
self.version_class_name = options[:class_name] || "PaperTrail::Version"
|
75
123
|
|
76
|
-
|
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?
|
124
|
+
setup_paper_trail_options(options)
|
87
125
|
|
88
126
|
class_attribute :versions_association_name
|
89
127
|
self.versions_association_name = options[:versions] || :versions
|
@@ -92,14 +130,14 @@ module PaperTrail
|
|
92
130
|
|
93
131
|
# `has_many` syntax for specifying order uses a lambda in Rails 4
|
94
132
|
if ::ActiveRecord::VERSION::MAJOR >= 4
|
95
|
-
has_many
|
96
|
-
|
97
|
-
:
|
133
|
+
has_many versions_association_name,
|
134
|
+
-> { order(model.timestamp_sort_order) },
|
135
|
+
class_name: version_class_name, as: :item
|
98
136
|
else
|
99
|
-
has_many
|
100
|
-
:
|
101
|
-
:
|
102
|
-
:
|
137
|
+
has_many versions_association_name,
|
138
|
+
class_name: version_class_name,
|
139
|
+
as: :item,
|
140
|
+
order: paper_trail_version_class.timestamp_sort_order
|
103
141
|
end
|
104
142
|
|
105
143
|
# Reset the transaction id when the transaction is closed.
|
@@ -108,6 +146,22 @@ module PaperTrail
|
|
108
146
|
after_rollback :clear_rolled_back_versions
|
109
147
|
end
|
110
148
|
|
149
|
+
# Given `options`, populates `paper_trail_options`.
|
150
|
+
# @api private
|
151
|
+
def setup_paper_trail_options(options)
|
152
|
+
class_attribute :paper_trail_options
|
153
|
+
self.paper_trail_options = options.dup
|
154
|
+
[:ignore, :skip, :only].each do |k|
|
155
|
+
paper_trail_options[k] = [paper_trail_options[k]].flatten.compact.map { |attr|
|
156
|
+
attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s
|
157
|
+
}
|
158
|
+
end
|
159
|
+
paper_trail_options[:meta] ||= {}
|
160
|
+
if paper_trail_options[:save_changes].nil?
|
161
|
+
paper_trail_options[:save_changes] = true
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
111
165
|
def setup_callbacks_from_options(options_on = [])
|
112
166
|
options_on.each do |option|
|
113
167
|
send "paper_trail_on_#{option}"
|
@@ -115,13 +169,13 @@ module PaperTrail
|
|
115
169
|
end
|
116
170
|
|
117
171
|
# Record version before or after "destroy" event
|
118
|
-
def paper_trail_on_destroy(recording_order =
|
119
|
-
unless %w
|
120
|
-
|
172
|
+
def paper_trail_on_destroy(recording_order = "before")
|
173
|
+
unless %w(after before).include?(recording_order.to_s)
|
174
|
+
raise ArgumentError, 'recording order can only be "after" or "before"'
|
121
175
|
end
|
122
176
|
|
123
|
-
if recording_order
|
124
|
-
|
177
|
+
if recording_order == "after" &&
|
178
|
+
Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("5")
|
125
179
|
if ::ActiveRecord::Base.belongs_to_required_by_default
|
126
180
|
::ActiveSupport::Deprecation.warn(
|
127
181
|
"paper_trail_on_destroy(:after) is incompatible with ActiveRecord " +
|
@@ -131,7 +185,7 @@ module PaperTrail
|
|
131
185
|
end
|
132
186
|
end
|
133
187
|
|
134
|
-
send "#{recording_order}_destroy", :record_destroy, :
|
188
|
+
send "#{recording_order}_destroy", :record_destroy, if: :save_version?
|
135
189
|
|
136
190
|
return if paper_trail_options[:on].include?(:destroy)
|
137
191
|
paper_trail_options[:on] << :destroy
|
@@ -139,10 +193,8 @@ module PaperTrail
|
|
139
193
|
|
140
194
|
# Record version after "update" event
|
141
195
|
def paper_trail_on_update
|
142
|
-
before_save
|
143
|
-
|
144
|
-
after_update :record_update,
|
145
|
-
:if => :save_version?
|
196
|
+
before_save :reset_timestamp_attrs_for_update_if_needed!, on: :update
|
197
|
+
after_update :record_update, if: :save_version?
|
146
198
|
after_update :clear_version_instance!
|
147
199
|
|
148
200
|
return if paper_trail_options[:on].include?(:update)
|
@@ -151,8 +203,7 @@ module PaperTrail
|
|
151
203
|
|
152
204
|
# Record version after "create" event
|
153
205
|
def paper_trail_on_create
|
154
|
-
after_create :record_create,
|
155
|
-
:if => :save_version?
|
206
|
+
after_create :record_create, if: :save_version?
|
156
207
|
|
157
208
|
return if paper_trail_options[:on].include?(:create)
|
158
209
|
paper_trail_options[:on] << :create
|
@@ -169,7 +220,7 @@ module PaperTrail
|
|
169
220
|
end
|
170
221
|
|
171
222
|
def paper_trail_enabled_for_model?
|
172
|
-
return false unless
|
223
|
+
return false unless include?(PaperTrail::Model::InstanceMethods)
|
173
224
|
PaperTrail.enabled_for_model?(self)
|
174
225
|
end
|
175
226
|
|
@@ -194,34 +245,43 @@ module PaperTrail
|
|
194
245
|
|
195
246
|
def originator
|
196
247
|
::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
|
197
|
-
|
248
|
+
paper_trail_originator
|
198
249
|
end
|
199
250
|
|
200
251
|
# Invoked after rollbacks to ensure versions records are not created
|
201
|
-
# for changes that never actually took place
|
252
|
+
# for changes that never actually took place.
|
253
|
+
# Optimization: Use lazy `reset` instead of eager `reload` because, in
|
254
|
+
# many use cases, the association will not be used.
|
202
255
|
def clear_rolled_back_versions
|
203
|
-
send(self.class.versions_association_name).
|
256
|
+
send(self.class.versions_association_name).reset
|
204
257
|
end
|
205
258
|
|
206
259
|
# Returns the object (not a Version) as it was at the given timestamp.
|
207
|
-
def version_at(timestamp, reify_options={})
|
260
|
+
def version_at(timestamp, reify_options = {})
|
208
261
|
# Because a version stores how its object looked *before* the change,
|
209
262
|
# we need to look for the first version created *after* the timestamp.
|
210
263
|
v = send(self.class.versions_association_name).subsequent(timestamp, true).first
|
211
264
|
return v.reify(reify_options) if v
|
212
|
-
self unless
|
265
|
+
self unless destroyed?
|
213
266
|
end
|
214
267
|
|
215
268
|
# Returns the objects (not Versions) as they were between the given times.
|
216
|
-
|
269
|
+
# TODO: Either add support for the third argument, `_reify_options`, or
|
270
|
+
# add a deprecation warning if someone tries to use it.
|
271
|
+
def versions_between(start_time, end_time, _reify_options = {})
|
217
272
|
versions = send(self.class.versions_association_name).between(start_time, end_time)
|
218
|
-
versions.collect { |version| version_at(version.send
|
273
|
+
versions.collect { |version| version_at(version.send(PaperTrail.timestamp_field)) }
|
219
274
|
end
|
220
275
|
|
221
276
|
# Returns the object (not a Version) as it was most recently.
|
222
277
|
def previous_version
|
223
|
-
|
224
|
-
|
278
|
+
previous =
|
279
|
+
if source_version
|
280
|
+
source_version.previous
|
281
|
+
else
|
282
|
+
send(self.class.versions_association_name).last
|
283
|
+
end
|
284
|
+
previous.try(:reify)
|
225
285
|
end
|
226
286
|
|
227
287
|
# Returns the object (not a Version) as it became next.
|
@@ -229,7 +289,7 @@ module PaperTrail
|
|
229
289
|
# "live" item, we return nil. Perhaps we should return self instead?
|
230
290
|
def next_version
|
231
291
|
subsequent_version = source_version.next
|
232
|
-
subsequent_version ? subsequent_version.reify : self.class.find(
|
292
|
+
subsequent_version ? subsequent_version.reify : self.class.find(id)
|
233
293
|
rescue
|
234
294
|
nil
|
235
295
|
end
|
@@ -240,7 +300,7 @@ module PaperTrail
|
|
240
300
|
|
241
301
|
# Executes the given method or block without creating a new version.
|
242
302
|
def without_versioning(method = nil)
|
243
|
-
paper_trail_was_enabled =
|
303
|
+
paper_trail_was_enabled = paper_trail_enabled_for_model?
|
244
304
|
self.class.paper_trail_off!
|
245
305
|
method ? method.to_proc.call(self) : yield(self)
|
246
306
|
ensure
|
@@ -249,6 +309,7 @@ module PaperTrail
|
|
249
309
|
|
250
310
|
# Utility method for reifying. Anything executed inside the block will
|
251
311
|
# appear like a new record.
|
312
|
+
# rubocop: disable Style/Alias
|
252
313
|
def appear_as_new_record
|
253
314
|
instance_eval {
|
254
315
|
alias :old_new_record? :new_record?
|
@@ -257,11 +318,12 @@ module PaperTrail
|
|
257
318
|
yield
|
258
319
|
instance_eval { alias :new_record? :old_new_record? }
|
259
320
|
end
|
321
|
+
# rubocop: enable Style/Alias
|
260
322
|
|
261
323
|
# Temporarily overwrites the value of whodunnit and then executes the
|
262
324
|
# provided block.
|
263
325
|
def whodunnit(value)
|
264
|
-
raise ArgumentError,
|
326
|
+
raise ArgumentError, "expected to receive a block" unless block_given?
|
265
327
|
current_whodunnit = PaperTrail.whodunnit
|
266
328
|
PaperTrail.whodunnit = value
|
267
329
|
yield self
|
@@ -287,7 +349,7 @@ module PaperTrail
|
|
287
349
|
attributes.each { |column| write_attribute(column, current_time) }
|
288
350
|
|
289
351
|
record_update(true) unless will_record_after_update?
|
290
|
-
save!(:
|
352
|
+
save!(validate: false)
|
291
353
|
end
|
292
354
|
|
293
355
|
private
|
@@ -306,8 +368,8 @@ module PaperTrail
|
|
306
368
|
def record_create
|
307
369
|
if paper_trail_switched_on?
|
308
370
|
data = {
|
309
|
-
:
|
310
|
-
:
|
371
|
+
event: paper_trail_event || "create",
|
372
|
+
whodunnit: PaperTrail.whodunnit
|
311
373
|
}
|
312
374
|
if respond_to?(:updated_at)
|
313
375
|
data[PaperTrail.timestamp_field] = updated_at
|
@@ -315,11 +377,11 @@ module PaperTrail
|
|
315
377
|
if pt_record_object_changes? && changed_notably?
|
316
378
|
data[:object_changes] = pt_recordable_object_changes
|
317
379
|
end
|
318
|
-
if self.class.paper_trail_version_class.column_names.include?(
|
380
|
+
if self.class.paper_trail_version_class.column_names.include?("transaction_id")
|
319
381
|
data[:transaction_id] = PaperTrail.transaction_id
|
320
382
|
end
|
321
383
|
version = send(self.class.versions_association_name).create! merge_metadata(data)
|
322
|
-
|
384
|
+
update_transaction_id(version)
|
323
385
|
save_associations(version)
|
324
386
|
end
|
325
387
|
end
|
@@ -327,9 +389,9 @@ module PaperTrail
|
|
327
389
|
def record_update(force = nil)
|
328
390
|
if paper_trail_switched_on? && (force || changed_notably?)
|
329
391
|
data = {
|
330
|
-
:
|
331
|
-
:
|
332
|
-
:
|
392
|
+
event: paper_trail_event || "update",
|
393
|
+
object: pt_recordable_object,
|
394
|
+
whodunnit: PaperTrail.whodunnit
|
333
395
|
}
|
334
396
|
if respond_to?(:updated_at)
|
335
397
|
data[PaperTrail.timestamp_field] = updated_at
|
@@ -337,11 +399,11 @@ module PaperTrail
|
|
337
399
|
if pt_record_object_changes?
|
338
400
|
data[:object_changes] = pt_recordable_object_changes
|
339
401
|
end
|
340
|
-
if self.class.paper_trail_version_class.column_names.include?(
|
402
|
+
if self.class.paper_trail_version_class.column_names.include?("transaction_id")
|
341
403
|
data[:transaction_id] = PaperTrail.transaction_id
|
342
404
|
end
|
343
405
|
version = send(self.class.versions_association_name).create merge_metadata(data)
|
344
|
-
|
406
|
+
update_transaction_id(version)
|
345
407
|
save_associations(version)
|
346
408
|
end
|
347
409
|
end
|
@@ -351,7 +413,7 @@ module PaperTrail
|
|
351
413
|
# @api private
|
352
414
|
def pt_record_object_changes?
|
353
415
|
paper_trail_options[:save_changes] &&
|
354
|
-
self.class.paper_trail_version_class.column_names.include?(
|
416
|
+
self.class.paper_trail_version_class.column_names.include?("object_changes")
|
355
417
|
end
|
356
418
|
|
357
419
|
# Returns an object which can be assigned to the `object` attribute of a
|
@@ -361,11 +423,10 @@ module PaperTrail
|
|
361
423
|
# `PaperTrail.serializer`.
|
362
424
|
# @api private
|
363
425
|
def pt_recordable_object
|
364
|
-
object_attrs = object_attrs_for_paper_trail(attributes_before_change)
|
365
426
|
if self.class.paper_trail_version_class.object_col_is_json?
|
366
|
-
|
427
|
+
object_attrs_for_paper_trail
|
367
428
|
else
|
368
|
-
PaperTrail.serializer.dump(
|
429
|
+
PaperTrail.serializer.dump(object_attrs_for_paper_trail)
|
369
430
|
end
|
370
431
|
end
|
371
432
|
|
@@ -384,9 +445,9 @@ module PaperTrail
|
|
384
445
|
end
|
385
446
|
|
386
447
|
def changes_for_paper_trail
|
387
|
-
|
388
|
-
self.class.serialize_attribute_changes_for_paper_trail!(
|
389
|
-
|
448
|
+
notable_changes = changes.delete_if { |k, _v| !notably_changed.include?(k) }
|
449
|
+
self.class.serialize_attribute_changes_for_paper_trail!(notable_changes)
|
450
|
+
notable_changes.to_hash
|
390
451
|
end
|
391
452
|
|
392
453
|
# Invoked via`after_update` callback for when a previous version is
|
@@ -398,7 +459,7 @@ module PaperTrail
|
|
398
459
|
# Invoked via callback when a user attempts to persist a reified
|
399
460
|
# `Version`.
|
400
461
|
def reset_timestamp_attrs_for_update_if_needed!
|
401
|
-
return if
|
462
|
+
return if live?
|
402
463
|
timestamp_attributes_for_update_in_model.each do |column|
|
403
464
|
# ActiveRecord 4.2 deprecated `reset_column!` in favor of
|
404
465
|
# `restore_column!`.
|
@@ -411,21 +472,21 @@ module PaperTrail
|
|
411
472
|
end
|
412
473
|
|
413
474
|
def record_destroy
|
414
|
-
if paper_trail_switched_on?
|
475
|
+
if paper_trail_switched_on? && !new_record?
|
415
476
|
data = {
|
416
|
-
:
|
417
|
-
:
|
418
|
-
:
|
419
|
-
:
|
420
|
-
:
|
477
|
+
item_id: id,
|
478
|
+
item_type: self.class.base_class.name,
|
479
|
+
event: paper_trail_event || "destroy",
|
480
|
+
object: pt_recordable_object,
|
481
|
+
whodunnit: PaperTrail.whodunnit
|
421
482
|
}
|
422
|
-
if self.class.paper_trail_version_class.column_names.include?(
|
483
|
+
if self.class.paper_trail_version_class.column_names.include?("transaction_id")
|
423
484
|
data[:transaction_id] = PaperTrail.transaction_id
|
424
485
|
end
|
425
486
|
version = self.class.paper_trail_version_class.create(merge_metadata(data))
|
426
487
|
send("#{self.class.version_association_name}=", version)
|
427
488
|
send(self.class.versions_association_name).send :load_target
|
428
|
-
|
489
|
+
update_transaction_id(version)
|
429
490
|
save_associations(version)
|
430
491
|
end
|
431
492
|
end
|
@@ -433,31 +494,50 @@ module PaperTrail
|
|
433
494
|
# Saves associations if the join table for `VersionAssociation` exists.
|
434
495
|
def save_associations(version)
|
435
496
|
return unless PaperTrail.config.track_associations?
|
497
|
+
save_associations_belongs_to(version)
|
498
|
+
save_associations_has_and_belongs_to_many(version)
|
499
|
+
end
|
500
|
+
|
501
|
+
def save_associations_belongs_to(version)
|
436
502
|
self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
437
503
|
assoc_version_args = {
|
438
|
-
|
439
|
-
|
504
|
+
version_id: version.id,
|
505
|
+
foreign_key_name: assoc.foreign_key
|
440
506
|
}
|
441
507
|
|
442
508
|
if assoc.options[:polymorphic]
|
443
509
|
associated_record = send(assoc.name) if send(assoc.foreign_type)
|
444
510
|
if associated_record && associated_record.class.paper_trail_enabled_for_model?
|
445
|
-
assoc_version_args
|
511
|
+
assoc_version_args[:foreign_key_id] = associated_record.id
|
446
512
|
end
|
447
513
|
elsif assoc.klass.paper_trail_enabled_for_model?
|
448
|
-
assoc_version_args
|
514
|
+
assoc_version_args[:foreign_key_id] = send(assoc.foreign_key)
|
449
515
|
end
|
450
516
|
|
451
|
-
|
517
|
+
if assoc_version_args.key?(:foreign_key_id)
|
518
|
+
PaperTrail::VersionAssociation.create(assoc_version_args)
|
519
|
+
end
|
452
520
|
end
|
453
521
|
end
|
454
522
|
|
455
|
-
def
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
523
|
+
def save_associations_has_and_belongs_to_many(version)
|
524
|
+
# Use the :added and :removed keys to extrapolate the HABTM associations
|
525
|
+
# to before any changes were made
|
526
|
+
self.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
|
527
|
+
next unless
|
528
|
+
self.class.paper_trail_save_join_tables.include?(a.name) ||
|
529
|
+
a.klass.paper_trail_enabled_for_model?
|
530
|
+
assoc_version_args = {
|
531
|
+
version_id: version.id,
|
532
|
+
foreign_key_name: a.name
|
533
|
+
}
|
534
|
+
assoc_ids =
|
535
|
+
send(a.name).to_a.map(&:id) +
|
536
|
+
(@paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) -
|
537
|
+
(@paper_trail_habtm.try(:[], a.name).try(:[], :added) || [])
|
538
|
+
assoc_ids.each do |id|
|
539
|
+
PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id))
|
540
|
+
end
|
461
541
|
end
|
462
542
|
end
|
463
543
|
|
@@ -467,14 +547,14 @@ module PaperTrail
|
|
467
547
|
|
468
548
|
def merge_metadata(data)
|
469
549
|
# First we merge the model-level metadata in `meta`.
|
470
|
-
paper_trail_options[:meta].each do |k,v|
|
550
|
+
paper_trail_options[:meta].each do |k, v|
|
471
551
|
data[k] =
|
472
552
|
if v.respond_to?(:call)
|
473
553
|
v.call(self)
|
474
|
-
elsif v.is_a?(Symbol) && respond_to?(v)
|
554
|
+
elsif v.is_a?(Symbol) && respond_to?(v, true)
|
475
555
|
# If it is an attribute that is changing in an existing object,
|
476
556
|
# be sure to grab the current version.
|
477
|
-
if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] !=
|
557
|
+
if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] != "create"
|
478
558
|
send("#{v}_was".to_sym)
|
479
559
|
else
|
480
560
|
send(v)
|
@@ -489,19 +569,14 @@ module PaperTrail
|
|
489
569
|
end
|
490
570
|
|
491
571
|
def attributes_before_change
|
492
|
-
|
493
|
-
|
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
|
572
|
+
changed = changed_attributes.select { |k, _v| self.class.column_names.include?(k) }
|
573
|
+
attributes.merge(changed)
|
499
574
|
end
|
500
575
|
|
501
576
|
# Returns hash of attributes (with appropriate attributes serialized),
|
502
577
|
# ommitting attributes to be skipped.
|
503
|
-
def object_attrs_for_paper_trail
|
504
|
-
attrs =
|
578
|
+
def object_attrs_for_paper_trail
|
579
|
+
attrs = attributes_before_change.except(*paper_trail_options[:skip])
|
505
580
|
self.class.serialize_attributes_for_paper_trail!(attrs)
|
506
581
|
attrs
|
507
582
|
end
|
@@ -522,40 +597,58 @@ module PaperTrail
|
|
522
597
|
# and/or the `:skip` option. Returns true if an ignored attribute has
|
523
598
|
# changed.
|
524
599
|
def ignored_attr_has_changed?
|
525
|
-
ignored =
|
600
|
+
ignored = paper_trail_options[:ignore] + paper_trail_options[:skip]
|
526
601
|
ignored.any? && (changed & ignored).any?
|
527
602
|
end
|
528
603
|
|
529
604
|
def notably_changed
|
530
|
-
only =
|
605
|
+
only = paper_trail_options[:only].dup
|
531
606
|
# Remove Hash arguments and then evaluate whether the attributes (the
|
532
607
|
# keys of the hash) should also get pushed into the collection.
|
533
608
|
only.delete_if do |obj|
|
534
|
-
obj.is_a?(Hash) &&
|
609
|
+
obj.is_a?(Hash) &&
|
610
|
+
obj.each { |attr, condition|
|
611
|
+
only << attr if condition.respond_to?(:call) && condition.call(self)
|
612
|
+
}
|
535
613
|
end
|
536
614
|
only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
|
537
615
|
end
|
538
616
|
|
539
617
|
def changed_and_not_ignored
|
540
|
-
ignore =
|
618
|
+
ignore = paper_trail_options[:ignore].dup
|
541
619
|
# Remove Hash arguments and then evaluate whether the attributes (the
|
542
620
|
# keys of the hash) should also get pushed into the collection.
|
543
621
|
ignore.delete_if do |obj|
|
544
|
-
obj.is_a?(Hash) &&
|
622
|
+
obj.is_a?(Hash) &&
|
623
|
+
obj.each { |attr, condition|
|
624
|
+
ignore << attr if condition.respond_to?(:call) && condition.call(self)
|
625
|
+
}
|
545
626
|
end
|
546
|
-
skip =
|
627
|
+
skip = paper_trail_options[:skip]
|
547
628
|
changed - ignore - skip
|
548
629
|
end
|
549
630
|
|
550
631
|
def paper_trail_switched_on?
|
551
|
-
PaperTrail.enabled? &&
|
632
|
+
PaperTrail.enabled? &&
|
633
|
+
PaperTrail.enabled_for_controller? &&
|
634
|
+
paper_trail_enabled_for_model?
|
552
635
|
end
|
553
636
|
|
554
637
|
def save_version?
|
555
|
-
if_condition
|
556
|
-
unless_condition =
|
638
|
+
if_condition = paper_trail_options[:if]
|
639
|
+
unless_condition = paper_trail_options[:unless]
|
557
640
|
(if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
|
558
641
|
end
|
642
|
+
|
643
|
+
# @api private
|
644
|
+
def update_transaction_id(version)
|
645
|
+
return unless self.class.paper_trail_version_class.column_names.include?("transaction_id")
|
646
|
+
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
|
647
|
+
PaperTrail.transaction_id = version.id
|
648
|
+
version.transaction_id = version.id
|
649
|
+
version.save
|
650
|
+
end
|
651
|
+
end
|
559
652
|
end
|
560
653
|
end
|
561
654
|
end
|