paper_trail 4.2.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|