paper_trail 9.2.0 → 12.1.0

Sign up to get free protection for your applications and to get access to all the features.
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