paper_trail 5.2.3 → 11.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 (198) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install/USAGE +3 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +75 -0
  4. data/lib/generators/paper_trail/{templates/add_object_changes_to_versions.rb → install/templates/add_object_changes_to_versions.rb.erb} +1 -1
  5. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
  10. data/lib/paper_trail.rb +82 -130
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +15 -44
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
  15. data/lib/paper_trail/cleaner.rb +3 -1
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +11 -49
  18. data/lib/paper_trail/events/base.rb +323 -0
  19. data/lib/paper_trail/events/create.rb +32 -0
  20. data/lib/paper_trail/events/destroy.rb +42 -0
  21. data/lib/paper_trail/events/update.rb +60 -0
  22. data/lib/paper_trail/frameworks/active_record.rb +2 -1
  23. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +8 -3
  24. data/lib/paper_trail/frameworks/cucumber.rb +5 -3
  25. data/lib/paper_trail/frameworks/rails.rb +2 -0
  26. data/lib/paper_trail/frameworks/rails/controller.rb +33 -43
  27. data/lib/paper_trail/frameworks/rails/engine.rb +34 -1
  28. data/lib/paper_trail/frameworks/rspec.rb +17 -4
  29. data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
  30. data/lib/paper_trail/has_paper_trail.rb +22 -310
  31. data/lib/paper_trail/model_config.rb +157 -109
  32. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  33. data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
  34. data/lib/paper_trail/record_history.rb +3 -9
  35. data/lib/paper_trail/record_trail.rb +169 -319
  36. data/lib/paper_trail/reifier.rb +53 -374
  37. data/lib/paper_trail/request.rb +166 -0
  38. data/lib/paper_trail/serializers/json.rb +9 -10
  39. data/lib/paper_trail/serializers/yaml.rb +15 -28
  40. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
  41. data/lib/paper_trail/version_concern.rb +160 -155
  42. data/lib/paper_trail/version_number.rb +12 -4
  43. metadata +77 -372
  44. data/.github/CONTRIBUTING.md +0 -109
  45. data/.github/ISSUE_TEMPLATE.md +0 -13
  46. data/.gitignore +0 -23
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -99
  49. data/.rubocop_todo.yml +0 -22
  50. data/.travis.yml +0 -41
  51. data/Appraisals +0 -38
  52. data/CHANGELOG.md +0 -560
  53. data/Gemfile +0 -2
  54. data/MIT-LICENSE +0 -20
  55. data/README.md +0 -1654
  56. data/Rakefile +0 -30
  57. data/doc/bug_report_template.rb +0 -69
  58. data/doc/warning_about_not_setting_whodunnit.md +0 -32
  59. data/gemfiles/ar3.gemfile +0 -19
  60. data/gemfiles/ar4.gemfile +0 -8
  61. data/gemfiles/ar5.gemfile +0 -9
  62. data/lib/generators/paper_trail/USAGE +0 -2
  63. data/lib/generators/paper_trail/default_initializer.rb +0 -0
  64. data/lib/generators/paper_trail/install_generator.rb +0 -57
  65. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -13
  66. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -22
  67. data/lib/generators/paper_trail/templates/create_versions.rb +0 -80
  68. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
  69. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -11
  70. data/lib/paper_trail/frameworks/sinatra.rb +0 -40
  71. data/lib/paper_trail/version_association_concern.rb +0 -17
  72. data/paper_trail.gemspec +0 -56
  73. data/spec/generators/install_generator_spec.rb +0 -66
  74. data/spec/generators/paper_trail/templates/create_versions_spec.rb +0 -51
  75. data/spec/models/animal_spec.rb +0 -36
  76. data/spec/models/boolit_spec.rb +0 -48
  77. data/spec/models/callback_modifier_spec.rb +0 -96
  78. data/spec/models/car_spec.rb +0 -13
  79. data/spec/models/custom_primary_key_record_spec.rb +0 -18
  80. data/spec/models/fluxor_spec.rb +0 -17
  81. data/spec/models/gadget_spec.rb +0 -68
  82. data/spec/models/joined_version_spec.rb +0 -47
  83. data/spec/models/json_version_spec.rb +0 -102
  84. data/spec/models/kitchen/banana_spec.rb +0 -14
  85. data/spec/models/not_on_update_spec.rb +0 -22
  86. data/spec/models/post_with_status_spec.rb +0 -50
  87. data/spec/models/skipper_spec.rb +0 -46
  88. data/spec/models/thing_spec.rb +0 -11
  89. data/spec/models/truck_spec.rb +0 -5
  90. data/spec/models/vehicle_spec.rb +0 -5
  91. data/spec/models/version_spec.rb +0 -272
  92. data/spec/models/widget_spec.rb +0 -343
  93. data/spec/modules/paper_trail_spec.rb +0 -27
  94. data/spec/modules/version_concern_spec.rb +0 -31
  95. data/spec/modules/version_number_spec.rb +0 -43
  96. data/spec/paper_trail/config_spec.rb +0 -33
  97. data/spec/paper_trail_spec.rb +0 -79
  98. data/spec/rails_helper.rb +0 -34
  99. data/spec/requests/articles_spec.rb +0 -34
  100. data/spec/spec_helper.rb +0 -114
  101. data/spec/support/alt_db_init.rb +0 -54
  102. data/test/custom_json_serializer.rb +0 -13
  103. data/test/dummy/Rakefile +0 -7
  104. data/test/dummy/app/controllers/application_controller.rb +0 -33
  105. data/test/dummy/app/controllers/articles_controller.rb +0 -20
  106. data/test/dummy/app/controllers/test_controller.rb +0 -5
  107. data/test/dummy/app/controllers/widgets_controller.rb +0 -32
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/models/animal.rb +0 -6
  110. data/test/dummy/app/models/article.rb +0 -24
  111. data/test/dummy/app/models/authorship.rb +0 -5
  112. data/test/dummy/app/models/bar_habtm.rb +0 -4
  113. data/test/dummy/app/models/book.rb +0 -9
  114. data/test/dummy/app/models/boolit.rb +0 -4
  115. data/test/dummy/app/models/callback_modifier.rb +0 -45
  116. data/test/dummy/app/models/car.rb +0 -3
  117. data/test/dummy/app/models/cat.rb +0 -2
  118. data/test/dummy/app/models/chapter.rb +0 -9
  119. data/test/dummy/app/models/citation.rb +0 -5
  120. data/test/dummy/app/models/custom_primary_key_record.rb +0 -13
  121. data/test/dummy/app/models/customer.rb +0 -4
  122. data/test/dummy/app/models/document.rb +0 -4
  123. data/test/dummy/app/models/dog.rb +0 -2
  124. data/test/dummy/app/models/editor.rb +0 -4
  125. data/test/dummy/app/models/editorship.rb +0 -5
  126. data/test/dummy/app/models/elephant.rb +0 -3
  127. data/test/dummy/app/models/fluxor.rb +0 -3
  128. data/test/dummy/app/models/foo_habtm.rb +0 -5
  129. data/test/dummy/app/models/foo_widget.rb +0 -2
  130. data/test/dummy/app/models/fruit.rb +0 -5
  131. data/test/dummy/app/models/gadget.rb +0 -3
  132. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  133. data/test/dummy/app/models/legacy_widget.rb +0 -4
  134. data/test/dummy/app/models/line_item.rb +0 -4
  135. data/test/dummy/app/models/not_on_update.rb +0 -4
  136. data/test/dummy/app/models/order.rb +0 -5
  137. data/test/dummy/app/models/paragraph.rb +0 -5
  138. data/test/dummy/app/models/person.rb +0 -38
  139. data/test/dummy/app/models/post.rb +0 -3
  140. data/test/dummy/app/models/post_with_status.rb +0 -8
  141. data/test/dummy/app/models/protected_widget.rb +0 -3
  142. data/test/dummy/app/models/quotation.rb +0 -5
  143. data/test/dummy/app/models/section.rb +0 -6
  144. data/test/dummy/app/models/skipper.rb +0 -6
  145. data/test/dummy/app/models/song.rb +0 -41
  146. data/test/dummy/app/models/thing.rb +0 -3
  147. data/test/dummy/app/models/translation.rb +0 -4
  148. data/test/dummy/app/models/truck.rb +0 -4
  149. data/test/dummy/app/models/vehicle.rb +0 -4
  150. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  151. data/test/dummy/app/models/widget.rb +0 -16
  152. data/test/dummy/app/models/wotsit.rb +0 -8
  153. data/test/dummy/app/versions/custom_primary_key_record_version.rb +0 -3
  154. data/test/dummy/app/versions/joined_version.rb +0 -6
  155. data/test/dummy/app/versions/json_version.rb +0 -3
  156. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  157. data/test/dummy/app/versions/post_version.rb +0 -3
  158. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  159. data/test/dummy/config.ru +0 -4
  160. data/test/dummy/config/application.rb +0 -80
  161. data/test/dummy/config/boot.rb +0 -10
  162. data/test/dummy/config/database.mysql.yml +0 -19
  163. data/test/dummy/config/database.postgres.yml +0 -15
  164. data/test/dummy/config/database.sqlite.yml +0 -15
  165. data/test/dummy/config/environment.rb +0 -5
  166. data/test/dummy/config/environments/development.rb +0 -41
  167. data/test/dummy/config/environments/production.rb +0 -74
  168. data/test/dummy/config/environments/test.rb +0 -51
  169. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -9
  170. data/test/dummy/config/initializers/inflections.rb +0 -10
  171. data/test/dummy/config/initializers/mime_types.rb +0 -5
  172. data/test/dummy/config/initializers/paper_trail.rb +0 -9
  173. data/test/dummy/config/initializers/secret_token.rb +0 -9
  174. data/test/dummy/config/initializers/session_store.rb +0 -8
  175. data/test/dummy/config/locales/en.yml +0 -5
  176. data/test/dummy/config/routes.rb +0 -4
  177. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -361
  178. data/test/dummy/db/schema.rb +0 -288
  179. data/test/dummy/script/rails +0 -8
  180. data/test/functional/controller_test.rb +0 -90
  181. data/test/functional/enabled_for_controller_test.rb +0 -28
  182. data/test/functional/modular_sinatra_test.rb +0 -46
  183. data/test/functional/sinatra_test.rb +0 -51
  184. data/test/functional/thread_safety_test.rb +0 -46
  185. data/test/test_helper.rb +0 -127
  186. data/test/time_travel_helper.rb +0 -1
  187. data/test/unit/associations_test.rb +0 -1016
  188. data/test/unit/cleaner_test.rb +0 -188
  189. data/test/unit/inheritance_column_test.rb +0 -43
  190. data/test/unit/model_test.rb +0 -1489
  191. data/test/unit/protected_attrs_test.rb +0 -52
  192. data/test/unit/serializer_test.rb +0 -119
  193. data/test/unit/serializers/json_test.rb +0 -95
  194. data/test/unit/serializers/mixin_json_test.rb +0 -37
  195. data/test/unit/serializers/mixin_yaml_test.rb +0 -53
  196. data/test/unit/serializers/yaml_test.rb +0 -54
  197. data/test/unit/timestamp_test.rb +0 -41
  198. data/test/unit/version_test.rb +0 -119
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  # Represents the history of a single record.
3
5
  # @api private
@@ -26,7 +28,7 @@ module PaperTrail
26
28
  @versions.select(primary_key).order(primary_key.asc)
27
29
  else
28
30
  @versions.
29
- select([timestamp, primary_key]).
31
+ select([table[:created_at], primary_key]).
30
32
  order(@version_class.timestamp_sort_order)
31
33
  end
32
34
  end
@@ -45,13 +47,5 @@ module PaperTrail
45
47
  def table
46
48
  @version_class.arel_table
47
49
  end
48
-
49
- # @return - Arel::Attribute - Attribute representing the timestamp column
50
- # of the version table, usually named `created_at` (the rails convention)
51
- # but not always.
52
- # @api private
53
- def timestamp
54
- table[PaperTrail.timestamp_field]
55
- end
56
50
  end
57
51
  end
@@ -1,42 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/events/create"
4
+ require "paper_trail/events/destroy"
5
+ require "paper_trail/events/update"
6
+
1
7
  module PaperTrail
2
8
  # Represents the "paper trail" for a single record.
3
9
  class RecordTrail
10
+ RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
11
+
4
12
  def initialize(record)
5
13
  @record = record
6
14
  end
7
15
 
8
- # Utility method for reifying. Anything executed inside the block will
9
- # appear like a new record.
10
- def appear_as_new_record
11
- @record.instance_eval {
12
- alias :old_new_record? :new_record?
13
- alias :new_record? :present?
14
- }
15
- yield
16
- @record.instance_eval { alias :new_record? :old_new_record? }
17
- end
18
-
19
- def attributes_before_change
20
- changed = @record.changed_attributes.select { |k, _v|
21
- @record.class.column_names.include?(k)
22
- }
23
- @record.attributes.merge(changed)
24
- end
25
-
26
- def changed_and_not_ignored
27
- ignore = @record.paper_trail_options[:ignore].dup
28
- # Remove Hash arguments and then evaluate whether the attributes (the
29
- # keys of the hash) should also get pushed into the collection.
30
- ignore.delete_if do |obj|
31
- obj.is_a?(Hash) &&
32
- obj.each { |attr, condition|
33
- ignore << attr if condition.respond_to?(:call) && condition.call(@record)
34
- }
35
- end
36
- skip = @record.paper_trail_options[:skip]
37
- @record.changed - ignore - skip
38
- end
39
-
40
16
  # Invoked after rollbacks to ensure versions records are not created for
41
17
  # changes that never actually took place. Optimization: Use lazy `reset`
42
18
  # instead of eager `reload` because, in many use cases, the association will
@@ -51,43 +27,12 @@ module PaperTrail
51
27
  @record.send("#{@record.class.version_association_name}=", nil)
52
28
  end
53
29
 
54
- # Determines whether it is appropriate to generate a new version
55
- # instance. A timestamp-only update (e.g. only `updated_at` changed) is
56
- # considered notable unless an ignored attribute was also changed.
57
- def changed_notably?
58
- if ignored_attr_has_changed?
59
- timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
60
- (notably_changed - timestamps).any?
61
- else
62
- notably_changed.any?
63
- end
64
- end
65
-
30
+ # Is PT enabled for this particular record?
66
31
  # @api private
67
- def changes
68
- notable_changes = @record.changes.delete_if { |k, _v|
69
- !notably_changed.include?(k)
70
- }
71
- AttributeSerializers::ObjectChangesAttribute.
72
- new(@record.class).
73
- serialize(notable_changes)
74
- notable_changes.to_hash
75
- end
76
-
77
32
  def enabled?
78
- PaperTrail.enabled? && PaperTrail.enabled_for_controller? && enabled_for_model?
79
- end
80
-
81
- def enabled_for_model?
82
- @record.class.paper_trail.enabled?
83
- end
84
-
85
- # An attributed is "ignored" if it is listed in the `:ignore` option
86
- # and/or the `:skip` option. Returns true if an ignored attribute has
87
- # changed.
88
- def ignored_attr_has_changed?
89
- ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
90
- ignored.any? && (@record.changed & ignored).any?
33
+ PaperTrail.enabled? &&
34
+ PaperTrail.request.enabled? &&
35
+ PaperTrail.request.enabled_for_model?(@record.class)
91
36
  end
92
37
 
93
38
  # Returns true if this instance is the current, live one;
@@ -96,237 +41,143 @@ module PaperTrail
96
41
  source_version.nil?
97
42
  end
98
43
 
99
- # @api private
100
- def merge_metadata(data)
101
- # First we merge the model-level metadata in `meta`.
102
- @record.paper_trail_options[:meta].each do |k, v|
103
- data[k] =
104
- if v.respond_to?(:call)
105
- v.call(@record)
106
- elsif v.is_a?(Symbol) && @record.respond_to?(v, true)
107
- # If it is an attribute that is changing in an existing object,
108
- # be sure to grab the current version.
109
- if @record.has_attribute?(v) &&
110
- @record.send("#{v}_changed?".to_sym) &&
111
- data[:event] != "create"
112
- @record.send("#{v}_was".to_sym)
113
- else
114
- @record.send(v)
115
- end
116
- else
117
- v
118
- end
119
- end
120
-
121
- # Second we merge any extra data from the controller (if available).
122
- data.merge(PaperTrail.controller_info || {})
123
- end
124
-
125
44
  # Returns the object (not a Version) as it became next.
126
45
  # NOTE: if self (the item) was not reified from a version, i.e. it is the
127
46
  # "live" item, we return nil. Perhaps we should return self instead?
128
47
  def next_version
129
48
  subsequent_version = source_version.next
130
49
  subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
131
- rescue # TODO: Rescue something more specific
50
+ rescue StandardError # TODO: Rescue something more specific
132
51
  nil
133
52
  end
134
53
 
135
- def notably_changed
136
- only = @record.paper_trail_options[:only].dup
137
- # Remove Hash arguments and then evaluate whether the attributes (the
138
- # keys of the hash) should also get pushed into the collection.
139
- only.delete_if do |obj|
140
- obj.is_a?(Hash) &&
141
- obj.each { |attr, condition|
142
- only << attr if condition.respond_to?(:call) && condition.call(@record)
143
- }
144
- end
145
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
146
- end
147
-
148
- # Returns hash of attributes (with appropriate attributes serialized),
149
- # omitting attributes to be skipped.
150
- def object_attrs_for_paper_trail
151
- attrs = attributes_before_change.except(*@record.paper_trail_options[:skip])
152
- AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
153
- attrs
154
- end
155
-
156
54
  # Returns who put `@record` into its current state.
55
+ #
56
+ # @api public
157
57
  def originator
158
58
  (source_version || versions.last).try(:whodunnit)
159
59
  end
160
60
 
161
61
  # Returns the object (not a Version) as it was most recently.
62
+ #
63
+ # @api public
162
64
  def previous_version
163
65
  (source_version ? source_version.previous : versions.last).try(:reify)
164
66
  end
165
67
 
166
68
  def record_create
167
69
  return unless enabled?
168
- data = {
169
- event: @record.paper_trail_event || "create",
170
- whodunnit: PaperTrail.whodunnit
171
- }
172
- if @record.respond_to?(:updated_at)
173
- data[PaperTrail.timestamp_field] = @record.updated_at
174
- end
175
- if record_object_changes? && changed_notably?
176
- data[:object_changes] = recordable_object_changes
177
- end
178
- add_transaction_id_to(data)
179
- versions_assoc = @record.send(@record.class.versions_association_name)
180
- version = versions_assoc.create! merge_metadata(data)
181
- update_transaction_id(version)
182
- save_associations(version)
183
- end
184
70
 
185
- def record_destroy
186
- if enabled? && !@record.new_record?
187
- data = {
188
- item_id: @record.id,
189
- item_type: @record.class.base_class.name,
190
- event: @record.paper_trail_event || "destroy",
191
- object: recordable_object,
192
- whodunnit: PaperTrail.whodunnit
193
- }
194
- add_transaction_id_to(data)
195
- version = @record.class.paper_trail.version_class.create(merge_metadata(data))
196
- if version.errors.any?
197
- log_version_errors(version, :destroy)
198
- else
199
- @record.send("#{@record.class.version_association_name}=", version)
200
- @record.send(@record.class.versions_association_name).reset
201
- update_transaction_id(version)
202
- save_associations(version)
203
- end
71
+ build_version_on_create(in_after_callback: true).tap do |version|
72
+ version.save!
73
+ # Because the version object was created using version_class.new instead
74
+ # of versions_assoc.build?, the association cache is unaware. So, we
75
+ # invalidate the `versions` association cache with `reset`.
76
+ versions.reset
204
77
  end
205
78
  end
206
79
 
207
- # Returns a boolean indicating whether to store serialized version diffs
208
- # in the `object_changes` column of the version record.
80
+ # PT-AT extends this method to add its transaction id.
81
+ #
209
82
  # @api private
210
- def record_object_changes?
211
- @record.paper_trail_options[:save_changes] &&
212
- @record.class.paper_trail.version_class.column_names.include?("object_changes")
83
+ def data_for_create
84
+ {}
213
85
  end
214
86
 
215
- def record_update(force)
216
- if enabled? && (force || changed_notably?)
217
- data = {
218
- event: @record.paper_trail_event || "update",
219
- object: recordable_object,
220
- whodunnit: PaperTrail.whodunnit
221
- }
222
- if @record.respond_to?(:updated_at)
223
- data[PaperTrail.timestamp_field] = @record.updated_at
224
- end
225
- if record_object_changes?
226
- data[:object_changes] = recordable_object_changes
227
- end
228
- add_transaction_id_to(data)
229
- versions_assoc = @record.send(@record.class.versions_association_name)
230
- version = versions_assoc.create(merge_metadata(data))
231
- if version.errors.any?
232
- log_version_errors(version, :update)
233
- else
234
- update_transaction_id(version)
235
- save_associations(version)
236
- end
87
+ # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
88
+ #
89
+ # @api private
90
+ # @return - The created version object, so that plugins can use it, e.g.
91
+ # paper_trail-association_tracking
92
+ def record_destroy(recording_order)
93
+ return unless enabled? && !@record.new_record?
94
+ in_after_callback = recording_order == "after"
95
+ event = Events::Destroy.new(@record, in_after_callback)
96
+
97
+ # Merge data from `Event` with data from PT-AT. We no longer use
98
+ # `data_for_destroy` but PT-AT still does.
99
+ data = event.data.merge(data_for_destroy)
100
+
101
+ version = @record.class.paper_trail.version_class.create(data)
102
+ if version.errors.any?
103
+ log_version_errors(version, :destroy)
104
+ else
105
+ assign_and_reset_version_association(version)
106
+ version
237
107
  end
238
108
  end
239
109
 
240
- # Returns an object which can be assigned to the `object` attribute of a
241
- # nascent version record. If the `object` column is a postgres `json`
242
- # column, then a hash can be used in the assignment, otherwise the column
243
- # is a `text` column, and we must perform the serialization here, using
244
- # `PaperTrail.serializer`.
110
+ # PT-AT extends this method to add its transaction id.
111
+ #
245
112
  # @api private
246
- def recordable_object
247
- if @record.class.paper_trail.version_class.object_col_is_json?
248
- object_attrs_for_paper_trail
113
+ def data_for_destroy
114
+ {}
115
+ end
116
+
117
+ # @api private
118
+ # @return - The created version object, so that plugins can use it, e.g.
119
+ # paper_trail-association_tracking
120
+ def record_update(force:, in_after_callback:, is_touch:)
121
+ return unless enabled?
122
+
123
+ version = build_version_on_update(
124
+ force: force,
125
+ in_after_callback: in_after_callback,
126
+ is_touch: is_touch
127
+ )
128
+ return unless version
129
+
130
+ if version.save
131
+ # Because the version object was created using version_class.new instead
132
+ # of versions_assoc.build?, the association cache is unaware. So, we
133
+ # invalidate the `versions` association cache with `reset`.
134
+ versions.reset
135
+ version
249
136
  else
250
- PaperTrail.serializer.dump(object_attrs_for_paper_trail)
137
+ log_version_errors(version, :update)
251
138
  end
252
139
  end
253
140
 
254
- # Returns an object which can be assigned to the `object_changes`
255
- # attribute of a nascent version record. If the `object_changes` column is
256
- # a postgres `json` column, then a hash can be used in the assignment,
257
- # otherwise the column is a `text` column, and we must perform the
258
- # serialization here, using `PaperTrail.serializer`.
141
+ # PT-AT extends this method to add its transaction id.
142
+ #
259
143
  # @api private
260
- def recordable_object_changes
261
- if @record.class.paper_trail.version_class.object_changes_col_is_json?
262
- changes
144
+ def data_for_update
145
+ {}
146
+ end
147
+
148
+ # @api private
149
+ # @return - The created version object, so that plugins can use it, e.g.
150
+ # paper_trail-association_tracking
151
+ def record_update_columns(changes)
152
+ return unless enabled?
153
+ event = Events::Update.new(@record, false, false, changes)
154
+
155
+ # Merge data from `Event` with data from PT-AT. We no longer use
156
+ # `data_for_update_columns` but PT-AT still does.
157
+ data = event.data.merge(data_for_update_columns)
158
+
159
+ versions_assoc = @record.send(@record.class.versions_association_name)
160
+ version = versions_assoc.create(data)
161
+ if version.errors.any?
162
+ log_version_errors(version, :update)
263
163
  else
264
- PaperTrail.serializer.dump(changes)
164
+ version
265
165
  end
266
166
  end
267
167
 
168
+ # PT-AT extends this method to add its transaction id.
169
+ #
170
+ # @api private
171
+ def data_for_update_columns
172
+ {}
173
+ end
174
+
268
175
  # Invoked via callback when a user attempts to persist a reified
269
176
  # `Version`.
270
177
  def reset_timestamp_attrs_for_update_if_needed
271
178
  return if live?
272
179
  @record.send(:timestamp_attributes_for_update_in_model).each do |column|
273
- # ActiveRecord 4.2 deprecated `reset_column!` in favor of
274
- # `restore_column!`.
275
- if @record.respond_to?("restore_#{column}!")
276
- @record.send("restore_#{column}!")
277
- else
278
- @record.send("reset_#{column}!")
279
- end
280
- end
281
- end
282
-
283
- # Saves associations if the join table for `VersionAssociation` exists.
284
- def save_associations(version)
285
- return unless PaperTrail.config.track_associations?
286
- save_associations_belongs_to(version)
287
- save_associations_habtm(version)
288
- end
289
-
290
- def save_associations_belongs_to(version)
291
- @record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
292
- assoc_version_args = {
293
- version_id: version.id,
294
- foreign_key_name: assoc.foreign_key
295
- }
296
-
297
- if assoc.options[:polymorphic]
298
- associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type)
299
- if associated_record && associated_record.class.paper_trail.enabled?
300
- assoc_version_args[:foreign_key_id] = associated_record.id
301
- end
302
- elsif assoc.klass.paper_trail.enabled?
303
- assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key)
304
- end
305
-
306
- if assoc_version_args.key?(:foreign_key_id)
307
- PaperTrail::VersionAssociation.create(assoc_version_args)
308
- end
309
- end
310
- end
311
-
312
- def save_associations_habtm(version)
313
- # Use the :added and :removed keys to extrapolate the HABTM associations
314
- # to before any changes were made
315
- @record.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
316
- next unless
317
- @record.class.paper_trail_save_join_tables.include?(a.name) ||
318
- a.klass.paper_trail.enabled?
319
- assoc_version_args = {
320
- version_id: version.transaction_id,
321
- foreign_key_name: a.name
322
- }
323
- assoc_ids =
324
- @record.send(a.name).to_a.map(&:id) +
325
- (@record.paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) -
326
- (@record.paper_trail_habtm.try(:[], a.name).try(:[], :added) || [])
327
- assoc_ids.each do |id|
328
- PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id))
329
- end
180
+ @record.send("restore_#{column}!")
330
181
  end
331
182
  end
332
183
 
@@ -342,26 +193,41 @@ module PaperTrail
342
193
  version
343
194
  end
344
195
 
345
- # Mimics the `touch` method from `ActiveRecord::Persistence`, but also
346
- # creates a version. A version is created regardless of options such as
347
- # `:on`, `:if`, or `:unless`.
196
+ # Save, and create a version record regardless of options such as `:on`,
197
+ # `:if`, or `:unless`.
348
198
  #
349
- # TODO: look into leveraging the `after_touch` callback from
350
- # `ActiveRecord` to allow the regular `touch` method to generate a version
351
- # as normal. May make sense to switch the `record_update` method to
352
- # leverage an `after_update` callback anyways (likely for v4.0.0)
353
- def touch_with_version(name = nil)
354
- unless @record.persisted?
355
- raise ActiveRecordError, "can not touch on a new record object"
199
+ # Arguments are passed to `save`.
200
+ #
201
+ # This is an "update" event. That is, we record the same data we would in
202
+ # the case of a normal AR `update`.
203
+ def save_with_version(*args)
204
+ ::PaperTrail.request(enabled: false) do
205
+ @record.save(*args)
206
+ end
207
+ record_update(force: true, in_after_callback: false, is_touch: false)
208
+ end
209
+
210
+ # Like the `update_column` method from `ActiveRecord::Persistence`, but also
211
+ # creates a version to record those changes.
212
+ # @api public
213
+ def update_column(name, value)
214
+ update_columns(name => value)
215
+ end
216
+
217
+ # Like the `update_columns` method from `ActiveRecord::Persistence`, but also
218
+ # creates a version to record those changes.
219
+ # @api public
220
+ def update_columns(attributes)
221
+ # `@record.update_columns` skips dirty-tracking, so we can't just use
222
+ # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
223
+ # We need to build our own hash with the changes that will be made
224
+ # directly to the database.
225
+ changes = {}
226
+ attributes.each do |k, v|
227
+ changes[k] = [@record[k], v]
356
228
  end
357
- attributes = @record.send :timestamp_attributes_for_update_in_model
358
- attributes << name if name
359
- current_time = @record.send :current_time_from_proper_timezone
360
- attributes.each { |column|
361
- @record.send(:write_attribute, column, current_time)
362
- }
363
- @record.record_update(true) unless will_record_after_update?
364
- @record.save!(validate: false)
229
+ @record.update_columns(attributes)
230
+ record_update_columns(changes)
365
231
  end
366
232
 
367
233
  # Returns the object (not a Version) as it was at the given timestamp.
@@ -376,69 +242,53 @@ module PaperTrail
376
242
  # Returns the objects (not Versions) as they were between the given times.
377
243
  def versions_between(start_time, end_time)
378
244
  versions = send(@record.class.versions_association_name).between(start_time, end_time)
379
- versions.collect { |version|
380
- version_at(version.send(PaperTrail.timestamp_field))
381
- }
245
+ versions.collect { |version| version_at(version.created_at) }
382
246
  end
383
247
 
384
- # Executes the given method or block without creating a new version.
385
- def without_versioning(method = nil)
386
- paper_trail_was_enabled = enabled_for_model?
387
- @record.class.paper_trail.disable
388
- if method
389
- if respond_to?(method)
390
- public_send(method)
391
- else
392
- @record.send(method)
393
- end
394
- else
395
- yield @record
396
- end
397
- ensure
398
- @record.class.paper_trail.enable if paper_trail_was_enabled
248
+ private
249
+
250
+ # @api private
251
+ def assign_and_reset_version_association(version)
252
+ @record.send("#{@record.class.version_association_name}=", version)
253
+ @record.send(@record.class.versions_association_name).reset
399
254
  end
400
255
 
401
- # Temporarily overwrites the value of whodunnit and then executes the
402
- # provided block.
403
- def whodunnit(value)
404
- raise ArgumentError, "expected to receive a block" unless block_given?
405
- current_whodunnit = PaperTrail.whodunnit
406
- PaperTrail.whodunnit = value
407
- yield @record
408
- ensure
409
- PaperTrail.whodunnit = current_whodunnit
256
+ # @api private
257
+ def build_version_on_create(in_after_callback:)
258
+ event = Events::Create.new(@record, in_after_callback)
259
+
260
+ # Merge data from `Event` with data from PT-AT. We no longer use
261
+ # `data_for_create` but PT-AT still does.
262
+ data = event.data.merge!(data_for_create)
263
+
264
+ # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
265
+ @record.class.paper_trail.version_class.new(data)
410
266
  end
411
267
 
412
- private
268
+ # @api private
269
+ def build_version_on_update(force:, in_after_callback:, is_touch:)
270
+ event = Events::Update.new(@record, in_after_callback, is_touch, nil)
271
+ return unless force || event.changed_notably?
272
+
273
+ # Merge data from `Event` with data from PT-AT. We no longer use
274
+ # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
275
+ # instead of `merge`.
276
+ data = event.data.merge!(data_for_update)
413
277
 
414
- def add_transaction_id_to(data)
415
- return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
416
- data[:transaction_id] = PaperTrail.transaction_id
278
+ # Using `version_class.new` reduces memory usage compared to
279
+ # `versions_assoc.build`. It's a trade-off though. We have to clear
280
+ # the association cache (see `versions.reset`) and that could cause an
281
+ # additional query in certain applications.
282
+ @record.class.paper_trail.version_class.new(data)
417
283
  end
418
284
 
419
285
  def log_version_errors(version, action)
420
- version.logger.warn(
421
- "Unable to create version for #{action} of #{@record.class.name}" +
286
+ version.logger&.warn(
287
+ "Unable to create version for #{action} of #{@record.class.name}" \
422
288
  "##{@record.id}: " + version.errors.full_messages.join(", ")
423
289
  )
424
290
  end
425
291
 
426
- # Returns true if `save` will cause `record_update`
427
- # to be called via the `after_update` callback.
428
- def will_record_after_update?
429
- on = @record.paper_trail_options[:on]
430
- on.nil? || on.include?(:update)
431
- end
432
-
433
- def update_transaction_id(version)
434
- return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
435
- if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
436
- PaperTrail.transaction_id = version.id
437
- version.transaction_id = version.id
438
- version.save
439
- end
440
- end
441
-
442
292
  def version
443
293
  @record.public_send(@record.class.version_association_name)
444
294
  end