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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +28 -9
  3. data/.github/ISSUE_TEMPLATE.md +13 -0
  4. data/.gitignore +2 -1
  5. data/.rubocop.yml +100 -0
  6. data/.rubocop_todo.yml +14 -0
  7. data/.travis.yml +8 -9
  8. data/Appraisals +41 -0
  9. data/CHANGELOG.md +49 -9
  10. data/Gemfile +1 -1
  11. data/README.md +130 -109
  12. data/Rakefile +19 -19
  13. data/doc/bug_report_template.rb +20 -14
  14. data/gemfiles/ar3.gemfile +10 -53
  15. data/gemfiles/ar4.gemfile +7 -0
  16. data/gemfiles/ar5.gemfile +13 -0
  17. data/lib/generators/paper_trail/install_generator.rb +26 -18
  18. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +4 -2
  19. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
  20. data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
  21. data/lib/generators/paper_trail/templates/create_versions.rb +39 -5
  22. data/lib/paper_trail.rb +169 -146
  23. data/lib/paper_trail/attributes_serialization.rb +89 -17
  24. data/lib/paper_trail/cleaner.rb +15 -9
  25. data/lib/paper_trail/config.rb +28 -11
  26. data/lib/paper_trail/frameworks/active_record.rb +4 -0
  27. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
  28. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
  29. data/lib/paper_trail/frameworks/cucumber.rb +1 -0
  30. data/lib/paper_trail/frameworks/rails.rb +2 -7
  31. data/lib/paper_trail/frameworks/rails/controller.rb +29 -9
  32. data/lib/paper_trail/frameworks/rails/engine.rb +7 -1
  33. data/lib/paper_trail/frameworks/rspec.rb +5 -5
  34. data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
  35. data/lib/paper_trail/frameworks/sinatra.rb +6 -4
  36. data/lib/paper_trail/has_paper_trail.rb +199 -106
  37. data/lib/paper_trail/record_history.rb +1 -3
  38. data/lib/paper_trail/reifier.rb +297 -118
  39. data/lib/paper_trail/serializers/json.rb +3 -3
  40. data/lib/paper_trail/serializers/yaml.rb +27 -8
  41. data/lib/paper_trail/version_association_concern.rb +3 -1
  42. data/lib/paper_trail/version_concern.rb +75 -35
  43. data/lib/paper_trail/version_number.rb +6 -9
  44. data/paper_trail.gemspec +44 -51
  45. data/spec/generators/install_generator_spec.rb +24 -25
  46. data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
  47. data/spec/models/animal_spec.rb +12 -12
  48. data/spec/models/boolit_spec.rb +8 -8
  49. data/spec/models/callback_modifier_spec.rb +47 -47
  50. data/spec/models/car_spec.rb +13 -0
  51. data/spec/models/fluxor_spec.rb +3 -3
  52. data/spec/models/gadget_spec.rb +19 -19
  53. data/spec/models/joined_version_spec.rb +3 -3
  54. data/spec/models/json_version_spec.rb +23 -24
  55. data/spec/models/kitchen/banana_spec.rb +3 -3
  56. data/spec/models/not_on_update_spec.rb +7 -4
  57. data/spec/models/post_with_status_spec.rb +13 -3
  58. data/spec/models/skipper_spec.rb +10 -10
  59. data/spec/models/thing_spec.rb +4 -4
  60. data/spec/models/truck_spec.rb +5 -0
  61. data/spec/models/vehicle_spec.rb +5 -0
  62. data/spec/models/version_spec.rb +103 -59
  63. data/spec/models/widget_spec.rb +82 -52
  64. data/spec/modules/paper_trail_spec.rb +2 -2
  65. data/spec/modules/version_concern_spec.rb +11 -12
  66. data/spec/modules/version_number_spec.rb +2 -4
  67. data/spec/paper_trail/config_spec.rb +10 -29
  68. data/spec/paper_trail_spec.rb +16 -14
  69. data/spec/rails_helper.rb +10 -9
  70. data/spec/requests/articles_spec.rb +11 -7
  71. data/spec/spec_helper.rb +41 -22
  72. data/spec/support/alt_db_init.rb +8 -13
  73. data/test/custom_json_serializer.rb +3 -3
  74. data/test/dummy/Rakefile +2 -2
  75. data/test/dummy/app/controllers/application_controller.rb +21 -8
  76. data/test/dummy/app/controllers/articles_controller.rb +11 -8
  77. data/test/dummy/app/controllers/widgets_controller.rb +13 -12
  78. data/test/dummy/app/models/animal.rb +1 -1
  79. data/test/dummy/app/models/article.rb +19 -11
  80. data/test/dummy/app/models/authorship.rb +1 -1
  81. data/test/dummy/app/models/bar_habtm.rb +4 -0
  82. data/test/dummy/app/models/book.rb +4 -4
  83. data/test/dummy/app/models/boolit.rb +1 -1
  84. data/test/dummy/app/models/callback_modifier.rb +6 -6
  85. data/test/dummy/app/models/car.rb +3 -0
  86. data/test/dummy/app/models/chapter.rb +4 -4
  87. data/test/dummy/app/models/customer.rb +1 -1
  88. data/test/dummy/app/models/document.rb +2 -2
  89. data/test/dummy/app/models/editor.rb +1 -1
  90. data/test/dummy/app/models/foo_habtm.rb +4 -0
  91. data/test/dummy/app/models/fruit.rb +2 -2
  92. data/test/dummy/app/models/gadget.rb +1 -1
  93. data/test/dummy/app/models/kitchen/banana.rb +1 -1
  94. data/test/dummy/app/models/legacy_widget.rb +2 -2
  95. data/test/dummy/app/models/line_item.rb +1 -1
  96. data/test/dummy/app/models/not_on_update.rb +1 -1
  97. data/test/dummy/app/models/person.rb +6 -6
  98. data/test/dummy/app/models/post.rb +1 -1
  99. data/test/dummy/app/models/post_with_status.rb +1 -1
  100. data/test/dummy/app/models/quotation.rb +1 -1
  101. data/test/dummy/app/models/section.rb +1 -1
  102. data/test/dummy/app/models/skipper.rb +2 -2
  103. data/test/dummy/app/models/song.rb +13 -4
  104. data/test/dummy/app/models/thing.rb +2 -2
  105. data/test/dummy/app/models/translation.rb +2 -2
  106. data/test/dummy/app/models/truck.rb +4 -0
  107. data/test/dummy/app/models/vehicle.rb +4 -0
  108. data/test/dummy/app/models/whatchamajigger.rb +1 -1
  109. data/test/dummy/app/models/widget.rb +7 -6
  110. data/test/dummy/app/versions/joined_version.rb +4 -3
  111. data/test/dummy/app/versions/json_version.rb +1 -1
  112. data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
  113. data/test/dummy/app/versions/post_version.rb +2 -2
  114. data/test/dummy/config.ru +1 -1
  115. data/test/dummy/config/application.rb +20 -9
  116. data/test/dummy/config/boot.rb +5 -5
  117. data/test/dummy/config/environment.rb +1 -1
  118. data/test/dummy/config/environments/development.rb +4 -3
  119. data/test/dummy/config/environments/production.rb +3 -2
  120. data/test/dummy/config/environments/test.rb +15 -5
  121. data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
  122. data/test/dummy/config/initializers/paper_trail.rb +1 -2
  123. data/test/dummy/config/initializers/secret_token.rb +3 -1
  124. data/test/dummy/config/initializers/session_store.rb +1 -1
  125. data/test/dummy/config/routes.rb +2 -2
  126. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -74
  127. data/test/dummy/db/schema.rb +29 -6
  128. data/test/dummy/script/rails +6 -4
  129. data/test/functional/controller_test.rb +34 -35
  130. data/test/functional/enabled_for_controller_test.rb +6 -7
  131. data/test/functional/modular_sinatra_test.rb +43 -38
  132. data/test/functional/sinatra_test.rb +49 -40
  133. data/test/functional/thread_safety_test.rb +4 -6
  134. data/test/paper_trail_test.rb +15 -14
  135. data/test/test_helper.rb +68 -44
  136. data/test/time_travel_helper.rb +1 -15
  137. data/test/unit/associations_test.rb +517 -251
  138. data/test/unit/cleaner_test.rb +66 -60
  139. data/test/unit/inheritance_column_test.rb +17 -17
  140. data/test/unit/model_test.rb +611 -504
  141. data/test/unit/protected_attrs_test.rb +16 -12
  142. data/test/unit/serializer_test.rb +44 -43
  143. data/test/unit/serializers/json_test.rb +17 -18
  144. data/test/unit/serializers/mixin_json_test.rb +15 -14
  145. data/test/unit/serializers/mixin_yaml_test.rb +20 -16
  146. data/test/unit/serializers/yaml_test.rb +12 -13
  147. data/test/unit/timestamp_test.rb +10 -12
  148. data/test/unit/version_test.rb +7 -7
  149. metadata +92 -40
@@ -1,8 +1,8 @@
1
- require 'active_support/core_ext/object' # provides the `try` method
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
- ::Sinatra.register PaperTrail::Sinatra if defined?(::Sinatra)
38
+ if defined?(::Sinatra)
39
+ ::Sinatra.register(::PaperTrail::Sinatra)
38
40
  end
@@ -1,13 +1,15 @@
1
- require 'active_support/core_ext/object' # provides the `try` method
2
- require 'paper_trail/attributes_serialization'
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 self.version_association_name
119
+ attr_accessor version_association_name
72
120
 
73
121
  class_attribute :version_class_name
74
- self.version_class_name = options[:class_name] || 'PaperTrail::Version'
122
+ self.version_class_name = options[:class_name] || "PaperTrail::Version"
75
123
 
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?
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 self.versions_association_name,
96
- lambda { order(model.timestamp_sort_order) },
97
- :class_name => self.version_class_name, :as => :item
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 self.versions_association_name,
100
- :class_name => self.version_class_name,
101
- :as => :item,
102
- :order => self.paper_trail_version_class.timestamp_sort_order
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 = 'after')
119
- unless %w[after before].include?(recording_order.to_s)
120
- fail ArgumentError, 'recording order can only be "after" or "before"'
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.to_s == 'after' and
124
- Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5")
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, :if => :save_version?
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 :reset_timestamp_attrs_for_update_if_needed!,
143
- :on => :update
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 self.include?(PaperTrail::Model::InstanceMethods)
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
- self.paper_trail_originator
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).reload
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 self.destroyed?
265
+ self unless destroyed?
213
266
  end
214
267
 
215
268
  # Returns the objects (not Versions) as they were between the given times.
216
- def versions_between(start_time, end_time, reify_options={})
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 PaperTrail.timestamp_field) }
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
- preceding_version = source_version ? source_version.previous : send(self.class.versions_association_name).last
224
- preceding_version.reify if preceding_version
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(self.id)
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 = self.paper_trail_enabled_for_model?
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, 'expected to receive a block' unless block_given?
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!(:validate => false)
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
- :event => paper_trail_event || 'create',
310
- :whodunnit => PaperTrail.whodunnit
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?('transaction_id')
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
- set_transaction_id(version)
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
- :event => paper_trail_event || 'update',
331
- :object => pt_recordable_object,
332
- :whodunnit => PaperTrail.whodunnit
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?('transaction_id')
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
- set_transaction_id(version)
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?('object_changes')
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
- object_attrs
427
+ object_attrs_for_paper_trail
367
428
  else
368
- PaperTrail.serializer.dump(object_attrs)
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
- _changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
388
- self.class.serialize_attribute_changes_for_paper_trail!(_changes)
389
- _changes.to_hash
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 self.live?
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? and not new_record?
475
+ if paper_trail_switched_on? && !new_record?
415
476
  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
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?('transaction_id')
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
- set_transaction_id(version)
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
- :version_id => version.id,
439
- :foreign_key_name => assoc.foreign_key
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.merge!(:foreign_key_id => associated_record.id)
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.merge!(:foreign_key_id => send(assoc.foreign_key))
514
+ assoc_version_args[:foreign_key_id] = send(assoc.foreign_key)
449
515
  end
450
516
 
451
- PaperTrail::VersionAssociation.create(assoc_version_args) if assoc_version_args.has_key?(:foreign_key_id)
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 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
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] != 'create'
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
- 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
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(attributes_hash)
504
- attrs = attributes_hash.except(*self.paper_trail_options[:skip])
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 = self.paper_trail_options[:ignore] + self.paper_trail_options[:skip]
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 = self.paper_trail_options[:only].dup
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) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
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 = self.paper_trail_options[:ignore].dup
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) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
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 = self.paper_trail_options[: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? && PaperTrail.enabled_for_controller? && self.paper_trail_enabled_for_model?
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 = self.paper_trail_options[:if]
556
- unless_condition = self.paper_trail_options[:unless]
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