paper_trail 9.2.0 → 12.1.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/USAGE +3 -0
  4. data/lib/generators/paper_trail/{install_generator.rb → install/install_generator.rb} +15 -38
  5. data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
  6. data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +2 -2
  7. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  8. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  9. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  10. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +8 -46
  13. data/lib/paper_trail/compatibility.rb +51 -0
  14. data/lib/paper_trail/config.rb +9 -2
  15. data/lib/paper_trail/errors.rb +33 -0
  16. data/lib/paper_trail/events/base.rb +305 -0
  17. data/lib/paper_trail/events/create.rb +32 -0
  18. data/lib/paper_trail/events/destroy.rb +42 -0
  19. data/lib/paper_trail/events/update.rb +60 -0
  20. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  21. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  22. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  23. data/lib/paper_trail/frameworks/rails.rb +1 -2
  24. data/lib/paper_trail/has_paper_trail.rb +20 -17
  25. data/lib/paper_trail/model_config.rb +103 -71
  26. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  27. data/lib/paper_trail/queries/versions/where_object.rb +4 -1
  28. data/lib/paper_trail/queries/versions/where_object_changes.rb +8 -13
  29. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  30. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  31. data/lib/paper_trail/record_trail.rb +94 -411
  32. data/lib/paper_trail/reifier.rb +41 -25
  33. data/lib/paper_trail/request.rb +0 -3
  34. data/lib/paper_trail/serializers/json.rb +0 -10
  35. data/lib/paper_trail/serializers/yaml.rb +5 -12
  36. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  37. data/lib/paper_trail/version_concern.rb +141 -61
  38. data/lib/paper_trail/version_number.rb +2 -2
  39. data/lib/paper_trail.rb +16 -123
  40. metadata +159 -56
  41. data/lib/generators/paper_trail/USAGE +0 -2
  42. data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
@@ -4,27 +4,6 @@ module PaperTrail
4
4
  # Configures an ActiveRecord model, mostly at application boot time, but also
5
5
  # sometimes mid-request, with methods like enable/disable.
6
6
  class ModelConfig
7
- DPR_DISABLE = <<-STR.squish.freeze
8
- MyModel.paper_trail.disable is deprecated, use
9
- PaperTrail.request.disable_model(MyModel). This new API makes it clear
10
- that only the current request is affected, not all threads. Also, all
11
- other request-variables now go through the same `request` method, so this
12
- new API is more consistent.
13
- STR
14
- DPR_ENABLE = <<-STR.squish.freeze
15
- MyModel.paper_trail.enable is deprecated, use
16
- PaperTrail.request.enable_model(MyModel). This new API makes it clear
17
- that only the current request is affected, not all threads. Also, all
18
- other request-variables now go through the same `request` method, so this
19
- new API is more consistent.
20
- STR
21
- DPR_ENABLED = <<-STR.squish.freeze
22
- MyModel.paper_trail.enabled? is deprecated, use
23
- PaperTrail.request.enabled_for_model?(MyModel). This new API makes it clear
24
- that this is a setting specific to the current request, not all threads.
25
- Also, all other request-variables now go through the same `request`
26
- method, so this new API is more consistent.
27
- STR
28
7
  E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
29
8
  paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
30
9
  belongs_to_required_by_default. Use on_destroy(:before)
@@ -39,29 +18,21 @@ module PaperTrail
39
18
  `abstract_class`. This is fine, but all application models must be
40
19
  configured to use concrete (not abstract) version models.
41
20
  STR
21
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
22
+ Passing versions association name as `has_paper_trail versions: %{versions_name}`
23
+ is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
24
+ The hash you pass to `versions:` is now passed directly to `has_many`.
25
+ STR
26
+ DPR_CLASS_NAME_OPTION = <<~STR.squish
27
+ Passing Version class name as `has_paper_trail class_name: %{class_name}`
28
+ is deprecated. Use `has_paper_trail versions: {class_name: %{class_name}}`
29
+ instead. The hash you pass to `versions:` is now passed directly to `has_many`.
30
+ STR
42
31
 
43
32
  def initialize(model_class)
44
33
  @model_class = model_class
45
34
  end
46
35
 
47
- # @deprecated
48
- def disable
49
- ::ActiveSupport::Deprecation.warn(DPR_DISABLE, caller(1))
50
- ::PaperTrail.request.disable_model(@model_class)
51
- end
52
-
53
- # @deprecated
54
- def enable
55
- ::ActiveSupport::Deprecation.warn(DPR_ENABLE, caller(1))
56
- ::PaperTrail.request.enable_model(@model_class)
57
- end
58
-
59
- # @deprecated
60
- def enabled?
61
- ::ActiveSupport::Deprecation.warn(DPR_ENABLED, caller(1))
62
- ::PaperTrail.request.enabled_for_model?(@model_class)
63
- end
64
-
65
36
  # Adds a callback that records a version after a "create" event.
66
37
  #
67
38
  # @api public
@@ -77,13 +48,7 @@ module PaperTrail
77
48
  #
78
49
  # @api public
79
50
  def on_destroy(recording_order = "before")
80
- unless %w[after before].include?(recording_order.to_s)
81
- raise ArgumentError, 'recording order can only be "after" or "before"'
82
- end
83
-
84
- if recording_order.to_s == "after" && cannot_record_after_destroy?
85
- raise E_CANNOT_RECORD_AFTER_DESTROY
86
- end
51
+ assert_valid_recording_order_for_on_destroy(recording_order)
87
52
 
88
53
  @model_class.send(
89
54
  "#{recording_order}_destroy",
@@ -121,11 +86,18 @@ module PaperTrail
121
86
  end
122
87
 
123
88
  # Adds a callback that records a version after a "touch" event.
89
+ #
90
+ # Rails < 6.0 has a bug where dirty-tracking does not occur during
91
+ # a `touch`. (https://github.com/rails/rails/issues/33429) See also:
92
+ # https://github.com/paper-trail-gem/paper_trail/issues/1121
93
+ # https://github.com/paper-trail-gem/paper_trail/issues/1161
94
+ # https://github.com/paper-trail-gem/paper_trail/pull/1285
95
+ #
124
96
  # @api public
125
97
  def on_touch
126
98
  @model_class.after_touch { |r|
127
99
  r.paper_trail.record_update(
128
- force: true,
100
+ force: RAILS_LT_6_0,
129
101
  in_after_callback: true,
130
102
  is_touch: true
131
103
  )
@@ -145,52 +117,111 @@ module PaperTrail
145
117
  setup_callbacks_from_options options[:on]
146
118
  end
147
119
 
120
+ # @api private
148
121
  def version_class
149
- @_version_class ||= @model_class.version_class_name.constantize
122
+ @version_class ||= @model_class.version_class_name.constantize
150
123
  end
151
124
 
152
125
  private
153
126
 
154
- def active_record_gem_version
155
- Gem::Version.new(ActiveRecord::VERSION::STRING)
156
- end
127
+ RAILS_LT_6_0 = ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0")
128
+ private_constant :RAILS_LT_6_0
157
129
 
158
130
  # Raises an error if the provided class is an `abstract_class`.
159
131
  # @api private
160
132
  def assert_concrete_activerecord_class(class_name)
161
133
  if class_name.constantize.abstract_class?
162
- raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
134
+ raise Error, format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
135
+ end
136
+ end
137
+
138
+ # @api private
139
+ def assert_valid_recording_order_for_on_destroy(recording_order)
140
+ unless %w[after before].include?(recording_order.to_s)
141
+ raise ArgumentError, 'recording order can only be "after" or "before"'
142
+ end
143
+
144
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
145
+ raise Error, E_CANNOT_RECORD_AFTER_DESTROY
163
146
  end
164
147
  end
165
148
 
166
149
  def cannot_record_after_destroy?
167
- Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
168
- ::ActiveRecord::Base.belongs_to_required_by_default
150
+ ::ActiveRecord::Base.belongs_to_required_by_default
151
+ end
152
+
153
+ def check_version_class_name(options)
154
+ # @api private - `version_class_name`
155
+ @model_class.class_attribute :version_class_name
156
+ if options[:class_name]
157
+ ::ActiveSupport::Deprecation.warn(
158
+ format(
159
+ DPR_CLASS_NAME_OPTION,
160
+ class_name: options[:class_name].inspect
161
+ ),
162
+ caller(1)
163
+ )
164
+ options[:versions][:class_name] = options[:class_name]
165
+ end
166
+ @model_class.version_class_name = options[:versions][:class_name] || "PaperTrail::Version"
167
+ assert_concrete_activerecord_class(@model_class.version_class_name)
168
+ end
169
+
170
+ def check_versions_association_name(options)
171
+ # @api private - versions_association_name
172
+ @model_class.class_attribute :versions_association_name
173
+ @model_class.versions_association_name = options[:versions][:name] || :versions
174
+ end
175
+
176
+ def define_has_many_versions(options)
177
+ options = ensure_versions_option_is_hash(options)
178
+ check_version_class_name(options)
179
+ check_versions_association_name(options)
180
+ scope = get_versions_scope(options)
181
+ @model_class.has_many(
182
+ @model_class.versions_association_name,
183
+ scope,
184
+ class_name: @model_class.version_class_name,
185
+ as: :item,
186
+ **options[:versions].except(:name, :scope)
187
+ )
188
+ end
189
+
190
+ def ensure_versions_option_is_hash(options)
191
+ unless options[:versions].is_a?(Hash)
192
+ if options[:versions]
193
+ ::ActiveSupport::Deprecation.warn(
194
+ format(
195
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION,
196
+ versions_name: options[:versions].inspect
197
+ ),
198
+ caller(1)
199
+ )
200
+ end
201
+ options[:versions] = {
202
+ name: options[:versions]
203
+ }
204
+ end
205
+ options
206
+ end
207
+
208
+ def get_versions_scope(options)
209
+ options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
169
210
  end
170
211
 
171
212
  def setup_associations(options)
213
+ # @api private - version_association_name
172
214
  @model_class.class_attribute :version_association_name
173
215
  @model_class.version_association_name = options[:version] || :version
174
216
 
175
217
  # The version this instance was reified from.
218
+ # @api public
176
219
  @model_class.send :attr_accessor, @model_class.version_association_name
177
220
 
178
- @model_class.class_attribute :version_class_name
179
- @model_class.version_class_name = options[:class_name] || "PaperTrail::Version"
180
-
181
- @model_class.class_attribute :versions_association_name
182
- @model_class.versions_association_name = options[:versions] || :versions
183
-
221
+ # @api public - paper_trail_event
184
222
  @model_class.send :attr_accessor, :paper_trail_event
185
223
 
186
- assert_concrete_activerecord_class(@model_class.version_class_name)
187
-
188
- @model_class.has_many(
189
- @model_class.versions_association_name,
190
- -> { order(model.timestamp_sort_order) },
191
- class_name: @model_class.version_class_name,
192
- as: :item
193
- )
224
+ define_has_many_versions(options)
194
225
  end
195
226
 
196
227
  def setup_callbacks_from_options(options_on = [])
@@ -200,6 +231,10 @@ module PaperTrail
200
231
  end
201
232
 
202
233
  def setup_options(options)
234
+ # @api public - paper_trail_options - Let's encourage plugins to use eg.
235
+ # `paper_trail_options[:versions][:class_name]` rather than
236
+ # `version_class_name` because the former is documented and the latter is
237
+ # not.
203
238
  @model_class.class_attribute :paper_trail_options
204
239
  @model_class.paper_trail_options = options.dup
205
240
 
@@ -211,9 +246,6 @@ module PaperTrail
211
246
  end
212
247
 
213
248
  @model_class.paper_trail_options[:meta] ||= {}
214
- if @model_class.paper_trail_options[:save_changes].nil?
215
- @model_class.paper_trail_options[:save_changes] = true
216
- end
217
249
  end
218
250
  end
219
251
  end
@@ -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
@@ -18,7 +18,10 @@ module PaperTrail
18
18
 
19
19
  # @api private
20
20
  def execute
21
- case @version_model_class.columns_hash["object"].type
21
+ column = @version_model_class.columns_hash["object"]
22
+ raise Error, "where_object requires an object column" unless column
23
+
24
+ case column.type
22
25
  when :jsonb
23
26
  jsonb
24
27
  when :json
@@ -23,18 +23,23 @@ module PaperTrail
23
23
 
24
24
  # @api private
25
25
  def execute
26
- if PaperTrail.config.object_changes_adapter
26
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes)
27
27
  return PaperTrail.config.object_changes_adapter.where_object_changes(
28
28
  @version_model_class, @attributes
29
29
  )
30
30
  end
31
- case @version_model_class.columns_hash["object_changes"].type
31
+ column_type = @version_model_class.columns_hash["object_changes"].type
32
+ case column_type
32
33
  when :jsonb
33
34
  jsonb
34
35
  when :json
35
36
  json
36
37
  else
37
- text
38
+ raise UnsupportedColumnType.new(
39
+ method: "where_object_changes",
40
+ expected: "json or jsonb",
41
+ actual: column_type
42
+ )
38
43
  end
39
44
  end
40
45
 
@@ -59,16 +64,6 @@ module PaperTrail
59
64
  @attributes.each { |field, value| @attributes[field] = [value] }
60
65
  @version_model_class.where("object_changes @> ?", @attributes.to_json)
61
66
  end
62
-
63
- # @api private
64
- def text
65
- arel_field = @version_model_class.arel_table[:object_changes]
66
- where_conditions = @attributes.map { |field, value|
67
- ::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
68
- }
69
- where_conditions = where_conditions.reduce { |a, e| a.and(e) }
70
- @version_model_class.where(where_conditions)
71
- end
72
67
  end
73
68
  end
74
69
  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.concat([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.concat([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