paper_trail 1.4.0 → 17.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/lib/generators/paper_trail/install/USAGE +31 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +101 -0
  4. data/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  5. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +41 -0
  6. data/lib/generators/paper_trail/migration_generator.rb +65 -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 +86 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +40 -0
  10. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +41 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +48 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +51 -0
  15. data/lib/paper_trail/cleaner.rb +60 -0
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +41 -0
  18. data/lib/paper_trail/errors.rb +33 -0
  19. data/lib/paper_trail/events/base.rb +343 -0
  20. data/lib/paper_trail/events/create.rb +32 -0
  21. data/lib/paper_trail/events/destroy.rb +42 -0
  22. data/lib/paper_trail/events/update.rb +76 -0
  23. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +16 -0
  24. data/lib/paper_trail/frameworks/active_record.rb +12 -0
  25. data/lib/paper_trail/frameworks/cucumber.rb +33 -0
  26. data/lib/paper_trail/frameworks/rails/controller.rb +103 -0
  27. data/lib/paper_trail/frameworks/rails/railtie.rb +34 -0
  28. data/lib/paper_trail/frameworks/rails.rb +3 -0
  29. data/lib/paper_trail/frameworks/rspec/helpers.rb +29 -0
  30. data/lib/paper_trail/frameworks/rspec.rb +42 -0
  31. data/lib/paper_trail/has_paper_trail.rb +79 -82
  32. data/lib/paper_trail/model_config.rb +257 -0
  33. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  34. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  35. data/lib/paper_trail/queries/versions/where_object_changes.rb +70 -0
  36. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  37. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  38. data/lib/paper_trail/record_history.rb +51 -0
  39. data/lib/paper_trail/record_trail.rb +342 -0
  40. data/lib/paper_trail/reifier.rb +147 -0
  41. data/lib/paper_trail/request.rb +163 -0
  42. data/lib/paper_trail/serializers/json.rb +36 -0
  43. data/lib/paper_trail/serializers/yaml.rb +68 -0
  44. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +35 -0
  45. data/lib/paper_trail/version_concern.rb +406 -0
  46. data/lib/paper_trail/version_number.rb +23 -0
  47. data/lib/paper_trail.rb +128 -19
  48. metadata +444 -70
  49. data/.gitignore +0 -3
  50. data/README.md +0 -225
  51. data/Rakefile +0 -50
  52. data/VERSION +0 -1
  53. data/generators/paper_trail/USAGE +0 -2
  54. data/generators/paper_trail/paper_trail_generator.rb +0 -9
  55. data/generators/paper_trail/templates/create_versions.rb +0 -18
  56. data/init.rb +0 -1
  57. data/install.rb +0 -1
  58. data/lib/paper_trail/version.rb +0 -59
  59. data/paper_trail.gemspec +0 -67
  60. data/rails/init.rb +0 -1
  61. data/tasks/paper_trail_tasks.rake +0 -0
  62. data/test/database.yml +0 -18
  63. data/test/paper_trail_controller_test.rb +0 -70
  64. data/test/paper_trail_model_test.rb +0 -448
  65. data/test/paper_trail_schema_test.rb +0 -15
  66. data/test/schema.rb +0 -48
  67. data/test/schema_change.rb +0 -3
  68. data/test/test_helper.rb +0 -43
  69. data/uninstall.rb +0 -1
  70. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_attribute_changes` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereAttributeChanges
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attribute - An attribute that changed. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attribute)
15
+ @version_model_class = version_model_class
16
+ @attribute = attribute
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_attribute_changes)
22
+ return PaperTrail.config.object_changes_adapter.where_attribute_changes(
23
+ @version_model_class, @attribute
24
+ )
25
+ end
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_attribute_changes",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @api private
42
+ def json
43
+ sql = "object_changes -> ? IS NOT NULL"
44
+
45
+ @version_model_class.where(sql, @attribute)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereObject
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+ @attributes = attributes
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ column = @version_model_class.columns_hash["object"]
22
+ raise Error, "where_object requires an object column" unless column
23
+
24
+ case column.type
25
+ when :jsonb
26
+ jsonb
27
+ when :json
28
+ json
29
+ else
30
+ text
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # @api private
37
+ def json
38
+ predicates = []
39
+ values = []
40
+ @attributes.each do |field, value|
41
+ predicates.push "object->>? = ?"
42
+ values.push(field, value.to_s)
43
+ end
44
+ sql = predicates.join(" and ")
45
+ @version_model_class.where(sql, *values)
46
+ end
47
+
48
+ # @api private
49
+ def jsonb
50
+ @version_model_class.where("object @> ?", @attributes.to_json)
51
+ end
52
+
53
+ # @api private
54
+ def text
55
+ arel_field = @version_model_class.arel_table[:object]
56
+ where_conditions = @attributes.map { |field, value|
57
+ ::PaperTrail.serializer.where_object_condition(arel_field, field, value)
58
+ }
59
+ where_conditions = where_conditions.reduce { |a, e| a.and(e) }
60
+ @version_model_class.where(where_conditions)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object_changes` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereObjectChanges
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+
17
+ # Currently, this `deep_dup` is necessary because the `jsonb` branch
18
+ # modifies `@attributes`, and that would be a nasty surprise for
19
+ # consumers of this class.
20
+ # TODO: Stop modifying `@attributes`, then remove `deep_dup`.
21
+ @attributes = attributes.deep_dup
22
+ end
23
+
24
+ # @api private
25
+ def execute
26
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes)
27
+ return PaperTrail.config.object_changes_adapter.where_object_changes(
28
+ @version_model_class, @attributes
29
+ )
30
+ end
31
+ column_type = @version_model_class.columns_hash["object_changes"].type
32
+ case column_type
33
+ when :jsonb
34
+ jsonb
35
+ when :json
36
+ json
37
+ else
38
+ raise UnsupportedColumnType.new(
39
+ method: "where_object_changes",
40
+ expected: "json or jsonb",
41
+ actual: column_type
42
+ )
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # @api private
49
+ def json
50
+ predicates = []
51
+ values = []
52
+ @attributes.each do |field, value|
53
+ predicates.push(
54
+ "((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))"
55
+ )
56
+ values.push(field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%")
57
+ end
58
+ sql = predicates.join(" and ")
59
+ @version_model_class.where(sql, *values)
60
+ end
61
+
62
+ # @api private
63
+ def jsonb
64
+ @attributes.each { |field, value| @attributes[field] = [value] }
65
+ @version_model_class.where("object_changes @> ?", @attributes.to_json)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object_changes_from` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereObjectChangesFrom
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+ @attributes = attributes
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes_from)
22
+ return PaperTrail.config.object_changes_adapter.where_object_changes_from(
23
+ @version_model_class, @attributes
24
+ )
25
+ end
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_object_changes_from",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @api private
42
+ def json
43
+ predicates = []
44
+ values = []
45
+ @attributes.each do |field, value|
46
+ predicates.push(
47
+ "(object_changes->>? ILIKE ?)"
48
+ )
49
+ values.push(field, "[#{value.to_json},%")
50
+ end
51
+ sql = predicates.join(" and ")
52
+ @version_model_class.where(sql, *values)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object_changes_to` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereObjectChangesTo
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+ @attributes = attributes
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes_to)
22
+ return PaperTrail.config.object_changes_adapter.where_object_changes_to(
23
+ @version_model_class, @attributes
24
+ )
25
+ end
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_object_changes_to",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @api private
42
+ def json
43
+ predicates = []
44
+ values = []
45
+ @attributes.each do |field, value|
46
+ predicates.push(
47
+ "(object_changes->>? ILIKE ?)"
48
+ )
49
+ values.push(field, "[%#{value.to_json}]")
50
+ end
51
+ sql = predicates.join(" and ")
52
+ @version_model_class.where(sql, *values)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Represents the history of a single record.
5
+ # @api private
6
+ class RecordHistory
7
+ # @param versions - ActiveRecord::Relation - All versions of the record.
8
+ # @param version_class - Class - Usually PaperTrail::Version,
9
+ # but it could also be a custom version class.
10
+ # @api private
11
+ def initialize(versions, version_class)
12
+ @versions = versions
13
+ @version_class = version_class
14
+ end
15
+
16
+ # Returns ordinal position of `version` in `sequence`.
17
+ # @api private
18
+ def index(version)
19
+ sequence.to_a.index(version)
20
+ end
21
+
22
+ private
23
+
24
+ # Returns `@versions` in chronological order.
25
+ # @api private
26
+ def sequence
27
+ if @version_class.primary_key_is_int?
28
+ @versions.select(primary_key).order(primary_key.asc)
29
+ else
30
+ @versions.
31
+ select([table[:created_at], primary_key]).
32
+ order(@version_class.timestamp_sort_order)
33
+ end
34
+ end
35
+
36
+ # @return - Arel::Attribute - Attribute representing the primary key
37
+ # of the version table. The column's data type is usually a serial
38
+ # integer (the rails convention) but not always.
39
+ # @api private
40
+ def primary_key
41
+ table[@version_class.primary_key]
42
+ end
43
+
44
+ # @return - Arel::Table - The version table, usually named `versions`, but
45
+ # not always.
46
+ # @api private
47
+ def table
48
+ @version_class.arel_table
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,342 @@
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
+
7
+ module PaperTrail
8
+ # Represents the "paper trail" for a single record.
9
+ class RecordTrail
10
+ def initialize(record)
11
+ @record = record
12
+ end
13
+
14
+ # Invoked after rollbacks to ensure versions records are not created for
15
+ # changes that never actually took place. Optimization: Use lazy `reset`
16
+ # instead of eager `reload` because, in many use cases, the association will
17
+ # not be used.
18
+ def clear_rolled_back_versions
19
+ versions.reset
20
+ end
21
+
22
+ # Invoked via`after_update` callback for when a previous version is
23
+ # reified and then saved.
24
+ def clear_version_instance
25
+ @record.send(:"#{@record.class.version_association_name}=", nil)
26
+ end
27
+
28
+ # Returns true if this instance is the current, live one;
29
+ # returns false if this instance came from a previous version.
30
+ def live?
31
+ source_version.nil?
32
+ end
33
+
34
+ # Returns the object (not a Version) as it became next.
35
+ # NOTE: if self (the item) was not reified from a version, i.e. it is the
36
+ # "live" item, we return nil. Perhaps we should return self instead?
37
+ def next_version
38
+ subsequent_version = source_version.next
39
+ subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
40
+ rescue StandardError # TODO: Rescue something more specific
41
+ nil
42
+ end
43
+
44
+ # Returns who put `@record` into its current state.
45
+ #
46
+ # @api public
47
+ def originator
48
+ (source_version || versions.last).try(:whodunnit)
49
+ end
50
+
51
+ # Returns the object (not a Version) as it was most recently.
52
+ #
53
+ # @api public
54
+ def previous_version
55
+ (source_version ? source_version.previous : versions.last).try(:reify)
56
+ end
57
+
58
+ def record_create
59
+ return unless enabled?
60
+
61
+ build_version_on_create(in_after_callback: true).tap do |version|
62
+ version.save!
63
+ # Because the version object was created using version_class.new instead
64
+ # of versions_assoc.build?, the association cache is unaware. So, we
65
+ # invalidate the `versions` association cache with `reset`.
66
+ versions.reset
67
+ rescue StandardError => e
68
+ handle_version_errors e, version, :create
69
+ end
70
+ end
71
+
72
+ # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
73
+ #
74
+ # @api private
75
+ # @return - The created version object, so that plugins can use it, e.g.
76
+ # paper_trail-association_tracking
77
+ def record_destroy(recording_order)
78
+ return unless enabled? && !@record.new_record?
79
+ in_after_callback = recording_order == "after"
80
+ event = Events::Destroy.new(@record, in_after_callback)
81
+
82
+ # Merge data from `Event` with data from PT-AT. We no longer use
83
+ # `data_for_destroy` but PT-AT still does.
84
+ data = event.data.merge(data_for_destroy)
85
+
86
+ version = @record.class.paper_trail.version_class.new(data)
87
+ begin
88
+ version.save!
89
+ assign_and_reset_version_association(version)
90
+ version
91
+ rescue StandardError => e
92
+ handle_version_errors e, version, :destroy
93
+ end
94
+ end
95
+
96
+ # @api private
97
+ # @param force [boolean] Insert a `Version` even if `@record` has not
98
+ # `changed_notably?`.
99
+ # @param in_after_callback [boolean] True when called from an `after_update`
100
+ # or `after_touch` callback.
101
+ # @param is_touch [boolean] True when called from an `after_touch` callback.
102
+ # @return - The created version object, so that plugins can use it, e.g.
103
+ # paper_trail-association_tracking
104
+ def record_update(force:, in_after_callback:, is_touch:)
105
+ return unless enabled?
106
+
107
+ version = build_version_on_update(
108
+ force: force,
109
+ in_after_callback: in_after_callback,
110
+ is_touch: is_touch
111
+ )
112
+ return unless version
113
+
114
+ begin
115
+ version.save!
116
+ # Because the version object was created using version_class.new instead
117
+ # of versions_assoc.build?, the association cache is unaware. So, we
118
+ # invalidate the `versions` association cache with `reset`.
119
+ versions.reset
120
+ version
121
+ rescue StandardError => e
122
+ handle_version_errors e, version, :update
123
+ end
124
+ end
125
+
126
+ # Invoked via callback when a user attempts to persist a reified
127
+ # `Version`.
128
+ def reset_timestamp_attrs_for_update_if_needed
129
+ return if live?
130
+ @record.send(:timestamp_attributes_for_update_in_model).each do |column|
131
+ @record.send(:"restore_#{column}!")
132
+ end
133
+ end
134
+
135
+ # AR callback.
136
+ # @api private
137
+ def save_version?
138
+ if_condition = @record.paper_trail_options[:if]
139
+ unless_condition = @record.paper_trail_options[:unless]
140
+ (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
141
+ end
142
+
143
+ def source_version
144
+ version
145
+ end
146
+
147
+ # Save, and create a version record regardless of options such as `:on`,
148
+ # `:if`, or `:unless`.
149
+ #
150
+ # `in_after_callback`: Indicates if this method is being called within an
151
+ # `after` callback. Defaults to `false`.
152
+ # `options`: Optional arguments passed to `save`.
153
+ #
154
+ # This is an "update" event. That is, we record the same data we would in
155
+ # the case of a normal AR `update`.
156
+ def save_with_version(in_after_callback: false, **options)
157
+ ::PaperTrail.request(enabled: false) do
158
+ @record.save(**options)
159
+ end
160
+ record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
161
+ end
162
+
163
+ # Like the `update_column` method from `ActiveRecord::Persistence`, but also
164
+ # creates a version to record those changes.
165
+ # @api public
166
+ def update_column(name, value)
167
+ update_columns(name => value)
168
+ end
169
+
170
+ # Like the `update_columns` method from `ActiveRecord::Persistence`, but also
171
+ # creates a version to record those changes.
172
+ # @api public
173
+ def update_columns(attributes)
174
+ # `@record.update_columns` skips dirty-tracking, so we can't just use
175
+ # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
176
+ # We need to build our own hash with the changes that will be made
177
+ # directly to the database.
178
+ changes = {}
179
+ attributes.each do |k, v|
180
+ changes[k] = [@record[k], v]
181
+ end
182
+ @record.update_columns(attributes)
183
+ record_update_columns(changes)
184
+ end
185
+
186
+ # Returns the object (not a Version) as it was at the given timestamp.
187
+ def version_at(timestamp, reify_options = {})
188
+ # Because a version stores how its object looked *before* the change,
189
+ # we need to look for the first version created *after* the timestamp.
190
+ v = versions.subsequent(timestamp, true).first
191
+ return v.reify(reify_options) if v
192
+ @record unless @record.destroyed?
193
+ end
194
+
195
+ # Returns the objects (not Versions) as they were between the given times.
196
+ def versions_between(start_time, end_time)
197
+ versions = send(@record.class.versions_association_name).between(start_time, end_time)
198
+ versions.collect { |version| version_at(version.created_at) }
199
+ end
200
+
201
+ private
202
+
203
+ # @api private
204
+ def assign_and_reset_version_association(version)
205
+ @record.send(:"#{@record.class.version_association_name}=", version)
206
+ @record.send(@record.class.versions_association_name).reset
207
+ end
208
+
209
+ # @api private
210
+ def build_version_on_create(in_after_callback:)
211
+ event = Events::Create.new(@record, in_after_callback)
212
+
213
+ # Merge data from `Event` with data from PT-AT. We no longer use
214
+ # `data_for_create` but PT-AT still does.
215
+ data = event.data.merge!(data_for_create)
216
+
217
+ # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
218
+ @record.class.paper_trail.version_class.new(data)
219
+ end
220
+
221
+ # @api private
222
+ def build_version_on_update(force:, in_after_callback:, is_touch:)
223
+ event = Events::Update.new(@record, in_after_callback, is_touch, nil)
224
+ return unless force || event.changed_notably?
225
+ data = event.data
226
+
227
+ # Copy the (recently set) `updated_at` from the record to the `created_at`
228
+ # of the `Version`. Without this feature, these two timestamps would
229
+ # differ by a few milliseconds. To some people, it seems a little
230
+ # unnatural to tamper with creation timestamps in this way. But, this
231
+ # feature has existed for a long time, almost a decade now, and some users
232
+ # may rely on it now.
233
+ if @record.respond_to?(:updated_at) &&
234
+ @record.paper_trail_options[:synchronize_version_creation_timestamp] != false
235
+ data[:created_at] = @record.updated_at
236
+ end
237
+
238
+ # Merge data from `Event` with data from PT-AT. We no longer use
239
+ # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
240
+ # instead of `merge`.
241
+ data.merge!(data_for_update)
242
+
243
+ # Using `version_class.new` reduces memory usage compared to
244
+ # `versions_assoc.build`. It's a trade-off though. We have to clear
245
+ # the association cache (see `versions.reset`) and that could cause an
246
+ # additional query in certain applications.
247
+ @record.class.paper_trail.version_class.new(data)
248
+ end
249
+
250
+ # PT-AT extends this method to add its transaction id.
251
+ #
252
+ # @api public
253
+ def data_for_create
254
+ {}
255
+ end
256
+
257
+ # PT-AT extends this method to add its transaction id.
258
+ #
259
+ # @api public
260
+ def data_for_destroy
261
+ {}
262
+ end
263
+
264
+ # PT-AT extends this method to add its transaction id.
265
+ #
266
+ # @api public
267
+ def data_for_update
268
+ {}
269
+ end
270
+
271
+ # PT-AT extends this method to add its transaction id.
272
+ #
273
+ # @api public
274
+ def data_for_update_columns
275
+ {}
276
+ end
277
+
278
+ # Is PT enabled for this particular record?
279
+ # @api private
280
+ def enabled?
281
+ PaperTrail.enabled? &&
282
+ PaperTrail.request.enabled? &&
283
+ PaperTrail.request.enabled_for_model?(@record.class)
284
+ end
285
+
286
+ def log_version_errors(version, action)
287
+ version.logger&.warn(
288
+ "Unable to create version for #{action} of #{@record.class.name}" \
289
+ "##{@record.id}: " + version.errors.full_messages.join(", ")
290
+ )
291
+ end
292
+
293
+ # Centralized handler for version errors
294
+ # @api private
295
+ def handle_version_errors(e, version, action)
296
+ case PaperTrail.config.version_error_behavior
297
+ when :legacy
298
+ # legacy behavior was to raise on create and log on update/delete
299
+ if action == :create
300
+ raise e
301
+ else
302
+ log_version_errors(version, action)
303
+ end
304
+ when :log
305
+ log_version_errors(version, action)
306
+ when :exception
307
+ raise e
308
+ when :silent
309
+ # noop
310
+ end
311
+ end
312
+
313
+ # @api private
314
+ # @return - The created version object, so that plugins can use it, e.g.
315
+ # paper_trail-association_tracking
316
+ def record_update_columns(changes)
317
+ return unless enabled?
318
+ data = Events::Update.new(@record, false, false, changes).data
319
+
320
+ # Merge data from `Event` with data from PT-AT. We no longer use
321
+ # `data_for_update_columns` but PT-AT still does.
322
+ data.merge!(data_for_update_columns)
323
+
324
+ versions_assoc = @record.send(@record.class.versions_association_name)
325
+ version = versions_assoc.new(data)
326
+ begin
327
+ version.save!
328
+ version
329
+ rescue StandardError => e
330
+ handle_version_errors e, version, :update
331
+ end
332
+ end
333
+
334
+ def version
335
+ @record.public_send(@record.class.version_association_name)
336
+ end
337
+
338
+ def versions
339
+ @record.public_send(@record.class.versions_association_name)
340
+ end
341
+ end
342
+ end