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.
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