paper_trail 10.3.1 → 12.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +13 -7
  4. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +1 -1
  5. data/lib/generators/paper_trail/migration_generator.rb +5 -4
  6. data/lib/paper_trail.rb +12 -38
  7. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  8. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +8 -46
  9. data/lib/paper_trail/compatibility.rb +2 -2
  10. data/lib/paper_trail/config.rb +0 -33
  11. data/lib/paper_trail/events/base.rb +22 -41
  12. data/lib/paper_trail/events/destroy.rb +1 -1
  13. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  14. data/lib/paper_trail/frameworks/rails.rb +1 -2
  15. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  16. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  17. data/lib/paper_trail/has_paper_trail.rb +1 -1
  18. data/lib/paper_trail/model_config.rb +3 -7
  19. data/lib/paper_trail/queries/versions/where_object_changes.rb +1 -1
  20. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +65 -0
  21. data/lib/paper_trail/record_trail.rb +2 -4
  22. data/lib/paper_trail/reifier.rb +17 -19
  23. data/lib/paper_trail/serializers/json.rb +8 -0
  24. data/lib/paper_trail/serializers/yaml.rb +8 -0
  25. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -14
  26. data/lib/paper_trail/version_concern.rb +30 -26
  27. data/lib/paper_trail/version_number.rb +3 -3
  28. metadata +76 -39
  29. data/lib/paper_trail/frameworks/rails/engine.rb +0 -45
@@ -35,7 +35,7 @@ module PaperTrail
35
35
  #
36
36
  # @override
37
37
  def changes_in_latest_version
38
- @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h
38
+ @record.attributes.transform_values { |value| [value, nil] }
39
39
  end
40
40
  end
41
41
  end
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This file only needs to be loaded if the gem is being used outside of Rails,
4
- # since otherwise the model(s) will get loaded in via the `Rails::Engine`.
3
+ # Either ActiveRecord has already been loaded by the Lazy Load Hook in our
4
+ # Railtie, or else we load it now.
5
+ require "active_record"
6
+ ::PaperTrail::Compatibility.check_activerecord(::ActiveRecord.gem_version)
7
+
8
+ # Now we can load the parts of PT that depend on AR.
9
+ require "paper_trail/has_paper_trail"
10
+ require "paper_trail/reifier"
5
11
  require "paper_trail/frameworks/active_record/models/paper_trail/version"
12
+ ActiveRecord::Base.include PaperTrail::Model
@@ -1,4 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "paper_trail/frameworks/rails/controller"
4
- require "paper_trail/frameworks/rails/engine"
3
+ require "paper_trail/frameworks/rails/railtie"
@@ -25,9 +25,7 @@ module PaperTrail
25
25
  # @api public
26
26
  def user_for_paper_trail
27
27
  return unless defined?(current_user)
28
- ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
29
- rescue NoMethodError
30
- current_user
28
+ current_user.try(:id) || current_user
31
29
  end
32
30
 
33
31
  # Returns any information about the controller or request that you
@@ -103,9 +101,3 @@ module PaperTrail
103
101
  end
104
102
  end
105
103
  end
106
-
107
- if defined?(::ActionController)
108
- ::ActiveSupport.on_load(:action_controller) do
109
- include ::PaperTrail::Rails::Controller
110
- end
111
- end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Represents code to load within Rails framework. See documentation in
5
+ # `railties/lib/rails/railtie.rb`.
6
+ # @api private
7
+ class Railtie < ::Rails::Railtie
8
+ # PaperTrail only has one initializer.
9
+ #
10
+ # We specify `before: "load_config_initializers"` to ensure that the PT
11
+ # initializer happens before "app initializers" (those defined in
12
+ # the app's `config/initalizers`).
13
+ initializer "paper_trail", before: "load_config_initializers" do
14
+ # `on_load` is a "lazy load hook". It "declares a block that will be
15
+ # executed when a Rails component is fully loaded". (See
16
+ # `active_support/lazy_load_hooks.rb`)
17
+ ActiveSupport.on_load(:action_controller) do
18
+ require "paper_trail/frameworks/rails/controller"
19
+
20
+ # Mix our extensions into `ActionController::Base`, which is `self`
21
+ # because of the `class_eval` in `lazy_load_hooks.rb`.
22
+ include PaperTrail::Rails::Controller
23
+ end
24
+
25
+ ActiveSupport.on_load(:active_record) do
26
+ require "paper_trail/frameworks/active_record"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -12,7 +12,7 @@ module PaperTrail
12
12
  # `.paper_trail` and `#paper_trail`.
13
13
  module Model
14
14
  def self.included(base)
15
- base.send :extend, ClassMethods
15
+ base.extend ClassMethods
16
16
  end
17
17
 
18
18
  # :nodoc:
@@ -122,16 +122,13 @@ module PaperTrail
122
122
  setup_callbacks_from_options options[:on]
123
123
  end
124
124
 
125
+ # @api private
125
126
  def version_class
126
- @_version_class ||= @model_class.version_class_name.constantize
127
+ @version_class ||= @model_class.version_class_name.constantize
127
128
  end
128
129
 
129
130
  private
130
131
 
131
- def active_record_gem_version
132
- Gem::Version.new(ActiveRecord::VERSION::STRING)
133
- end
134
-
135
132
  # Raises an error if the provided class is an `abstract_class`.
136
133
  # @api private
137
134
  def assert_concrete_activerecord_class(class_name)
@@ -141,8 +138,7 @@ module PaperTrail
141
138
  end
142
139
 
143
140
  def cannot_record_after_destroy?
144
- Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
145
- ::ActiveRecord::Base.belongs_to_required_by_default
141
+ ::ActiveRecord::Base.belongs_to_required_by_default
146
142
  end
147
143
 
148
144
  # Some options require the presence of the `item_subtype` column. Currently
@@ -23,7 +23,7 @@ module PaperTrail
23
23
 
24
24
  # @api private
25
25
  def execute
26
- if PaperTrail.config.object_changes_adapter&.respond_to?(:where_object_changes)
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
  )
@@ -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_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
+
27
+ case @version_model_class.columns_hash["object_changes"].type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ text
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # @api private
38
+ def json
39
+ predicates = []
40
+ values = []
41
+ @attributes.each do |field, value|
42
+ predicates.push(
43
+ "(object_changes->>? ILIKE ?)"
44
+ )
45
+ values.concat([field, "[#{value.to_json},%"])
46
+ end
47
+ sql = predicates.join(" and ")
48
+ @version_model_class.where(sql, *values)
49
+ end
50
+
51
+ # @api private
52
+ def text
53
+ arel_field = @version_model_class.arel_table[:object_changes]
54
+
55
+ where_conditions = @attributes.map do |field, value|
56
+ ::PaperTrail.serializer.where_object_changes_from_condition(arel_field, field, value)
57
+ end
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
@@ -7,8 +7,6 @@ require "paper_trail/events/update"
7
7
  module PaperTrail
8
8
  # Represents the "paper trail" for a single record.
9
9
  class RecordTrail
10
- RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
11
-
12
10
  def initialize(record)
13
11
  @record = record
14
12
  end
@@ -200,9 +198,9 @@ module PaperTrail
200
198
  #
201
199
  # This is an "update" event. That is, we record the same data we would in
202
200
  # the case of a normal AR `update`.
203
- def save_with_version(*args)
201
+ def save_with_version(**options)
204
202
  ::PaperTrail.request(enabled: false) do
205
- @record.save(*args)
203
+ @record.save(**options)
206
204
  end
207
205
  record_update(force: true, in_after_callback: false, is_touch: false)
208
206
  end
@@ -52,23 +52,23 @@ module PaperTrail
52
52
  # not the actual subclass. If `type` is present but empty, the class is
53
53
  # the base class.
54
54
  def init_model(attrs, options, version)
55
- if options[:dup] != true && version.item
56
- model = version.item
57
- if options[:unversioned_attributes] == :nil
58
- init_unversioned_attrs(attrs, model)
59
- end
60
- else
61
- klass = version_reification_class(version, attrs)
62
- # The `dup` option always returns a new object, otherwise we should
63
- # attempt to look for the item outside of default scope(s).
64
- find_cond = { klass.primary_key => version.item_id }
65
- if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil?
66
- model = klass.new
67
- elsif options[:unversioned_attributes] == :nil
68
- model = item_found
69
- init_unversioned_attrs(attrs, model)
70
- end
55
+ klass = version_reification_class(version, attrs)
56
+
57
+ # The `dup` option and destroyed version always returns a new object,
58
+ # otherwise we should attempt to load item or to look for the item
59
+ # outside of default scope(s).
60
+ model = if options[:dup] == true || version.event == "destroy"
61
+ klass.new
62
+ else
63
+ find_cond = { klass.primary_key => version.item_id }
64
+
65
+ version.item || klass.unscoped.where(find_cond).first || klass.new
66
+ end
67
+
68
+ if options[:unversioned_attributes] == :nil && !model.new_record?
69
+ init_unversioned_attrs(attrs, model)
71
70
  end
71
+
72
72
  model
73
73
  end
74
74
 
@@ -88,9 +88,7 @@ module PaperTrail
88
88
  #
89
89
  # @api private
90
90
  def reify_attribute(k, v, model, version)
91
- enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
92
- is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
93
- if model.has_attribute?(k) && !is_enum_without_type_caster
91
+ if model.has_attribute?(k)
94
92
  model[k.to_sym] = v
95
93
  elsif model.respond_to?("#{k}=")
96
94
  model.send("#{k}=", v)
@@ -41,6 +41,14 @@ module PaperTrail
41
41
  discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
42
42
  STR
43
43
  end
44
+
45
+ # Raises an exception as this operation is not allowed from text columns.
46
+ def where_object_changes_from_condition(*)
47
+ raise <<-STR.squish.freeze
48
+ where_object_changes_from does not support reading JSON from a text
49
+ column. The json and jsonb datatypes are supported.
50
+ STR
51
+ end
44
52
  end
45
53
  end
46
54
  end
@@ -38,6 +38,14 @@ module PaperTrail
38
38
  discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
39
39
  STR
40
40
  end
41
+
42
+ # Raises an exception as this operation is not allowed with YAML.
43
+ def where_object_changes_from_condition(*)
44
+ raise <<-STR.squish.freeze
45
+ where_object_changes_from does not support reading YAML from a text
46
+ column. The json and jsonb datatypes are supported.
47
+ STR
48
+ end
41
49
  end
42
50
  end
43
51
  end
@@ -11,15 +11,12 @@ module PaperTrail
11
11
  end
12
12
 
13
13
  def serialize(array)
14
- return serialize_with_ar(array) if active_record_pre_502?
15
14
  array
16
15
  end
17
16
 
18
17
  def deserialize(array)
19
- return deserialize_with_ar(array) if active_record_pre_502?
20
-
21
18
  case array
22
- # Needed for legacy reasons. If serialized array is a string
19
+ # Needed for legacy data. If serialized array is a string
23
20
  # then it was serialized with Rails < 5.0.2.
24
21
  when ::String then deserialize_with_ar(array)
25
22
  else array
@@ -28,16 +25,6 @@ module PaperTrail
28
25
 
29
26
  private
30
27
 
31
- def active_record_pre_502?
32
- ::ActiveRecord.gem_version < Gem::Version.new("5.0.2")
33
- end
34
-
35
- def serialize_with_ar(array)
36
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
37
- new(@subtype, @delimiter).
38
- serialize(array)
39
- end
40
-
41
28
  def deserialize_with_ar(array)
42
29
  ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
43
30
  new(@subtype, @delimiter).
@@ -3,6 +3,7 @@
3
3
  require "paper_trail/attribute_serializers/object_changes_attribute"
4
4
  require "paper_trail/queries/versions/where_object"
5
5
  require "paper_trail/queries/versions/where_object_changes"
6
+ require "paper_trail/queries/versions/where_object_changes_from"
6
7
 
7
8
  module PaperTrail
8
9
  # Originally, PaperTrail did not provide this module, and all of this
@@ -13,12 +14,7 @@ module PaperTrail
13
14
  extend ::ActiveSupport::Concern
14
15
 
15
16
  included do
16
- if ::ActiveRecord.gem_version >= Gem::Version.new("5.0")
17
- belongs_to :item, polymorphic: true, optional: true
18
- else
19
- belongs_to :item, polymorphic: true
20
- end
21
-
17
+ belongs_to :item, polymorphic: true, optional: true
22
18
  validates_presence_of :event
23
19
  after_create :enforce_version_limit!
24
20
  end
@@ -120,6 +116,21 @@ module PaperTrail
120
116
  Queries::Versions::WhereObjectChanges.new(self, args).execute
121
117
  end
122
118
 
119
+ # Given a hash of attributes like `name: 'Joan'`, query the
120
+ # `versions.objects_changes` column for changes where the version changed
121
+ # from the hash of attributes to other values.
122
+ #
123
+ # This is useful for finding versions where the attribute started with a
124
+ # known value and changed to something else. This is in comparison to
125
+ # `where_object_changes` which will find both the changes before and
126
+ # after.
127
+ #
128
+ # @api public
129
+ def where_object_changes_from(args = {})
130
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
131
+ Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
132
+ end
133
+
123
134
  def primary_key_is_int?
124
135
  @primary_key_is_int ||= columns_hash[primary_key].type == :integer
125
136
  rescue StandardError # TODO: Rescue something more specific
@@ -145,6 +156,7 @@ module PaperTrail
145
156
  # Default: false.
146
157
  # @return `ActiveRecord::Relation`
147
158
  # @api public
159
+ # rubocop:disable Style/OptionalBooleanParameter
148
160
  def preceding(obj, timestamp_arg = false)
149
161
  if timestamp_arg != true && primary_key_is_int?
150
162
  preceding_by_id(obj)
@@ -152,6 +164,7 @@ module PaperTrail
152
164
  preceding_by_timestamp(obj)
153
165
  end
154
166
  end
167
+ # rubocop:enable Style/OptionalBooleanParameter
155
168
 
156
169
  # Returns versions after `obj`.
157
170
  #
@@ -160,6 +173,7 @@ module PaperTrail
160
173
  # Default: false.
161
174
  # @return `ActiveRecord::Relation`
162
175
  # @api public
176
+ # rubocop:disable Style/OptionalBooleanParameter
163
177
  def subsequent(obj, timestamp_arg = false)
164
178
  if timestamp_arg != true && primary_key_is_int?
165
179
  subsequent_by_id(obj)
@@ -167,6 +181,7 @@ module PaperTrail
167
181
  subsequent_by_timestamp(obj)
168
182
  end
169
183
  end
184
+ # rubocop:enable Style/OptionalBooleanParameter
170
185
 
171
186
  private
172
187
 
@@ -205,18 +220,8 @@ module PaperTrail
205
220
 
206
221
  # Restore the item from this version.
207
222
  #
208
- # Optionally this can also restore all :has_one and :has_many (including
209
- # has_many :through) associations as they were "at the time", if they are
210
- # also being versioned by PaperTrail.
211
- #
212
223
  # Options:
213
224
  #
214
- # - :has_one
215
- # - `true` - Also reify has_one associations.
216
- # - `false - Default.
217
- # - :has_many
218
- # - `true` - Also reify has_many and has_many :through associations.
219
- # - `false` - Default.
220
225
  # - :mark_for_destruction
221
226
  # - `true` - Mark the has_one/has_many associations that did not exist in
222
227
  # the reified version for destruction, instead of removing them.
@@ -258,13 +263,6 @@ module PaperTrail
258
263
  end
259
264
  alias version_author terminator
260
265
 
261
- def sibling_versions(reload = false)
262
- if reload || !defined?(@sibling_versions) || @sibling_versions.nil?
263
- @sibling_versions = self.class.with_item_keys(item_type, item_id)
264
- end
265
- @sibling_versions
266
- end
267
-
268
266
  def next
269
267
  @next ||= sibling_versions.subsequent(self).first
270
268
  end
@@ -274,8 +272,9 @@ module PaperTrail
274
272
  end
275
273
 
276
274
  # Returns an integer representing the chronological position of the
277
- # version among its siblings (see `sibling_versions`). The "create" event,
278
- # for example, has an index of 0.
275
+ # version among its siblings. The "create" event, for example, has an index
276
+ # of 0.
277
+ #
279
278
  # @api public
280
279
  def index
281
280
  @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
@@ -285,7 +284,7 @@ module PaperTrail
285
284
 
286
285
  # @api private
287
286
  def load_changeset
288
- if PaperTrail.config.object_changes_adapter&.respond_to?(:load_changeset)
287
+ if PaperTrail.config.object_changes_adapter.respond_to?(:load_changeset)
289
288
  return PaperTrail.config.object_changes_adapter.load_changeset(self)
290
289
  end
291
290
 
@@ -342,6 +341,11 @@ module PaperTrail
342
341
  excess_versions.map(&:destroy)
343
342
  end
344
343
 
344
+ # @api private
345
+ def sibling_versions
346
+ @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
347
+ end
348
+
345
349
  # See docs section 2.e. Limiting the Number of Versions Created.
346
350
  # The version limit can be global or per-model.
347
351
  #