paper_trail 10.1.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ac357832683a239eb259f30fea0d05701ddd634f
4
- data.tar.gz: 1745e41be7c599090c4fd947822a30a6ad18b6ee
2
+ SHA256:
3
+ metadata.gz: f8f7237a39b0645932b394ee0332846be680f6a725ed02c7a126aa892d840ccc
4
+ data.tar.gz: b0356d622a6e6f3ba0938c4d9fe0ff211b347a0c65e2d3a6574bc8bf5fb04df6
5
5
  SHA512:
6
- metadata.gz: efa1906f7569fd7cb9eab56ad5d70daed5739714cd52197e97e2057cdb311b3016c3b36c3b4992c1cb75cc26ede53e42b481424fedc4fc40f47adc3902c7f772
7
- data.tar.gz: b4e4976c7273d397ea6a657debd40b4aa4354bce68b59c7e70bb28cf928e46c210d4085e44314d3aa8b3d8889fb46762375987bf904a989751d732056f54b592
6
+ metadata.gz: 46ad3d73231d4ab6c3d978925e7b838af2697b2b998b9a7948d078266fbdbec33b6dbdcbfb93751f2aa961be6a3499f499a71d56e3fbc6138174cb752feae41a
7
+ data.tar.gz: 9ec2fc044229898aa2c5ab70dc41b549b6896c61af42633af0760e6b99aadd375f5f7e39160ad713131ecb439b0f5aff8cb6696f276f4c5225eb40e371f43035
@@ -25,10 +25,14 @@ module PaperTrail
25
25
  " See section 5.c. Generators in README.md for more information."
26
26
 
27
27
  def create_migration_file
28
- add_paper_trail_migration("create_versions",
28
+ add_paper_trail_migration(
29
+ "create_versions",
29
30
  item_type_options: item_type_options,
30
- versions_table_options: versions_table_options)
31
- add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes?
31
+ versions_table_options: versions_table_options
32
+ )
33
+ if options.with_changes?
34
+ add_paper_trail_migration("add_object_changes_to_versions")
35
+ end
32
36
  end
33
37
 
34
38
  private
@@ -11,7 +11,7 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
11
11
  def change
12
12
  create_table :versions<%= versions_table_options %> do |t|
13
13
  t.string :item_type<%= item_type_options %>
14
- t.integer :item_id, null: false
14
+ t.bigint :item_id, null: false
15
15
  t.string :event, null: false
16
16
  t.string :whodunnit
17
17
  t.text :object, limit: TEXT_BYTES
@@ -28,10 +28,11 @@ module PaperTrail
28
28
  end
29
29
 
30
30
  def migration_version
31
- major = ActiveRecord::VERSION::MAJOR
32
- if major >= 5
33
- "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
34
- end
31
+ format(
32
+ "[%d.%d]",
33
+ ActiveRecord::VERSION::MAJOR,
34
+ ActiveRecord::VERSION::MINOR
35
+ )
35
36
  end
36
37
  end
37
38
  end
@@ -16,6 +16,7 @@ require "active_record"
16
16
 
17
17
  require "request_store"
18
18
  require "paper_trail/cleaner"
19
+ require "paper_trail/compatibility"
19
20
  require "paper_trail/config"
20
21
  require "paper_trail/has_paper_trail"
21
22
  require "paper_trail/record_history"
@@ -125,6 +126,10 @@ module PaperTrail
125
126
  end
126
127
  end
127
128
 
129
+ # We use the `on_load` "hook" instead of `ActiveRecord::Base.include` because we
130
+ # don't want to cause all of AR to be autloaded yet. See
131
+ # https://guides.rubyonrails.org/engines.html#what-are-on-load-hooks-questionmark
132
+ # to learn more about `on_load`.
128
133
  ActiveSupport.on_load(:active_record) do
129
134
  include PaperTrail::Model
130
135
  end
@@ -141,3 +146,7 @@ if defined?(::Rails)
141
146
  else
142
147
  require "paper_trail/frameworks/active_record"
143
148
  end
149
+
150
+ if defined?(::ActiveRecord)
151
+ ::PaperTrail::Compatibility.check_activerecord(::ActiveRecord.gem_version)
152
+ end
@@ -32,50 +32,18 @@ module PaperTrail
32
32
  end
33
33
  end
34
34
 
35
- if ::ActiveRecord::VERSION::MAJOR >= 5
36
- # This implementation uses AR 5's `serialize` and `deserialize`.
37
- class CastAttributeSerializer
38
- def serialize(attr, val)
39
- AttributeSerializerFactory.for(@klass, attr).serialize(val)
40
- end
41
-
42
- def deserialize(attr, val)
43
- if defined_enums[attr] && val.is_a?(::String)
44
- # Because PT 4 used to save the string version of enums to `object_changes`
45
- val
46
- else
47
- AttributeSerializerFactory.for(@klass, attr).deserialize(val)
48
- end
49
- end
35
+ # Uses AR 5's `serialize` and `deserialize`.
36
+ class CastAttributeSerializer
37
+ def serialize(attr, val)
38
+ AttributeSerializerFactory.for(@klass, attr).serialize(val)
50
39
  end
51
- else
52
- # This implementation uses AR 4.2's `type_cast_for_database`. For
53
- # versions of AR < 4.2 we provide an implementation of
54
- # `type_cast_for_database` in our shim attribute type classes,
55
- # `NoOpAttribute` and `SerializedAttribute`.
56
- class CastAttributeSerializer
57
- def serialize(attr, val)
58
- castable_val = val
59
- if defined_enums[attr]
60
- # `attr` is an enum. Find the number that corresponds to `val`. If `val` is
61
- # a number already, there won't be a corresponding entry, just use `val`.
62
- castable_val = defined_enums[attr][val] || val
63
- end
64
- @klass.type_for_attribute(attr).type_cast_for_database(castable_val)
65
- end
66
40
 
67
- def deserialize(attr, val)
68
- if defined_enums[attr] && val.is_a?(::String)
69
- # Because PT 4 used to save the string version of enums to `object_changes`
70
- val
71
- else
72
- val = @klass.type_for_attribute(attr).type_cast_from_database(val)
73
- if defined_enums[attr]
74
- defined_enums[attr].key(val)
75
- else
76
- val
77
- end
78
- end
41
+ def deserialize(attr, val)
42
+ if defined_enums[attr] && val.is_a?(::String)
43
+ # Because PT 4 used to save the string version of enums to `object_changes`
44
+ val
45
+ else
46
+ AttributeSerializerFactory.for(@klass, attr).deserialize(val)
79
47
  end
80
48
  end
81
49
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Rails does not follow SemVer, makes breaking changes in minor versions.
5
+ # Breaking changes are expected, and are generally good for the rails
6
+ # ecosystem. However, they often require dozens of hours to fix, even with the
7
+ # [help of experts](https://github.com/paper-trail-gem/paper_trail/pull/899).
8
+ #
9
+ # It is not safe to assume that a new version of rails will be compatible with
10
+ # PaperTrail. PT is only compatible with the versions of rails that it is
11
+ # tested against. See `.travis.yml`.
12
+ #
13
+ # However, as of
14
+ # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
15
+ # gemspec allows installation with newer, incompatible rails versions. We hope
16
+ # this will make it easier for contributors to work on compatibility with
17
+ # newer rails versions. Most PT users should avoid incompatible rails
18
+ # versions.
19
+ module Compatibility
20
+ ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
21
+ ACTIVERECORD_LT = "< 6.1" # not enforced in gemspec
22
+
23
+ E_INCOMPATIBLE_AR = <<-EOS
24
+ PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
25
+ contributors to install incompatible versions of ActiveRecord, and this
26
+ warning can be silenced with an environment variable, but this is a bad
27
+ idea for normal use. Please install a compatible version of ActiveRecord
28
+ instead (%s). Please see the discussion in paper_trail/compatibility.rb
29
+ for details.
30
+ EOS
31
+
32
+ # Normal users need a warning if they accidentally install an incompatible
33
+ # version of ActiveRecord. Contributors can silence this warning with an
34
+ # environment variable.
35
+ def self.check_activerecord(ar_version)
36
+ raise ::TypeError unless ar_version.instance_of?(::Gem::Version)
37
+ return if ::ENV["PT_SILENCE_AR_COMPAT_WARNING"].present?
38
+ req = ::Gem::Requirement.new([ACTIVERECORD_GTE, ACTIVERECORD_LT])
39
+ unless req.satisfied_by?(ar_version)
40
+ ::Kernel.warn(
41
+ format(
42
+ E_INCOMPATIBLE_AR,
43
+ ::PaperTrail.gem_version,
44
+ ar_version,
45
+ req
46
+ )
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -9,14 +9,6 @@ module PaperTrail
9
9
  class Config
10
10
  include Singleton
11
11
 
12
- E_PT_AT_REMOVED = <<-EOS.squish
13
- Association Tracking for PaperTrail has been extracted to a separate gem.
14
- To use it, please add `paper_trail-association_tracking` to your Gemfile.
15
- If you don't use it (most people don't, that's the default) and you set
16
- `track_associations = false` somewhere (probably a rails initializer) you
17
- can remove that line now.
18
- EOS
19
-
20
12
  attr_accessor(
21
13
  :association_reify_error_behaviour,
22
14
  :object_changes_adapter,
@@ -43,30 +35,5 @@ module PaperTrail
43
35
  def enabled=(enable)
44
36
  @mutex.synchronize { @enabled = enable }
45
37
  end
46
-
47
- # In PT 10, the paper_trail-association_tracking gem was changed from a
48
- # runtime dependency to a development dependency. We raise an error about
49
- # this for the people who don't read changelogs.
50
- #
51
- # We raise a generic RuntimeError instead of a specific PT error class
52
- # because there is no known use case where someone would want to rescue
53
- # this. If we think of such a use case in the future we can revisit this
54
- # decision.
55
- #
56
- # @override If PT-AT is `require`d, it will replace this method with its
57
- # own implementation.
58
- def track_associations=(value)
59
- if value
60
- raise E_PT_AT_REMOVED
61
- else
62
- ::Kernel.warn(E_PT_AT_REMOVED)
63
- end
64
- end
65
-
66
- # @override If PT-AT is `require`d, it will replace this method with its
67
- # own implementation.
68
- def track_associations?
69
- raise E_PT_AT_REMOVED
70
- end
71
38
  end
72
39
  end
@@ -59,14 +59,29 @@ module PaperTrail
59
59
  end
60
60
 
61
61
  # @api private
62
- def attributes_before_change(is_touch)
63
- Hash[@record.attributes.map do |k, v|
64
- if @record.class.column_names.include?(k)
65
- [k, attribute_in_previous_version(k, is_touch)]
66
- else
67
- [k, v]
62
+ def nonskipped_attributes_before_change(is_touch)
63
+ cache_changed_attributes do
64
+ record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip])
65
+
66
+ record_attributes.each_key do |k|
67
+ if @record.class.column_names.include?(k)
68
+ record_attributes[k] = attribute_in_previous_version(k, is_touch)
69
+ end
68
70
  end
69
- end]
71
+ end
72
+ end
73
+
74
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`.
75
+ # @api private
76
+ def cache_changed_attributes(&block)
77
+ if RAILS_GTE_5_1
78
+ # Everything works fine as it is
79
+ yield
80
+ else
81
+ # Any particular call to `changed_attributes` produces the huge memory allocation.
82
+ # Lets use the generic AR workaround for that.
83
+ @record.send(:cache_changed_attributes, &block)
84
+ end
70
85
  end
71
86
 
72
87
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
@@ -92,7 +107,7 @@ module PaperTrail
92
107
  end
93
108
 
94
109
  # @api private
95
- def changed_and_not_ignored
110
+ def calculated_ignored_array
96
111
  ignore = @record.paper_trail_options[:ignore].dup
97
112
  # Remove Hash arguments and then evaluate whether the attributes (the
98
113
  # keys of the hash) should also get pushed into the collection.
@@ -102,42 +117,18 @@ module PaperTrail
102
117
  ignore << attr if condition.respond_to?(:call) && condition.call(@record)
103
118
  }
104
119
  end
105
- skip = @record.paper_trail_options[:skip]
106
- (changed_in_latest_version - ignore) - skip
107
120
  end
108
121
 
109
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
110
- # https://github.com/paper-trail-gem/paper_trail/pull/899
111
- #
112
122
  # @api private
113
- def changed_in_latest_version
114
- if @in_after_callback && RAILS_GTE_5_1
115
- @record.saved_changes.keys
116
- else
117
- @record.changed
118
- end
119
- end
120
-
121
- # @api private
122
- def prepare_object_changes(changes)
123
- changes = serialize_object_changes(changes)
124
- changes = recordable_object_changes(changes)
125
- changes
126
- end
127
-
128
- # @api private
129
- def serialize_object_changes(changes)
130
- AttributeSerializers::ObjectChangesAttribute.
131
- new(@record.class).
132
- serialize(changes)
133
- changes.to_hash
123
+ def changed_and_not_ignored
124
+ skip = @record.paper_trail_options[:skip]
125
+ (changed_in_latest_version - calculated_ignored_array) - skip
134
126
  end
135
127
 
136
128
  # @api private
137
- def notable_changes
138
- changes_in_latest_version.delete_if { |k, _v|
139
- !notably_changed.include?(k)
140
- }
129
+ def changed_in_latest_version
130
+ # Memoized to reduce memory usage
131
+ @changed_in_latest_version ||= changes_in_latest_version.keys
141
132
  end
142
133
 
143
134
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
@@ -145,10 +136,13 @@ module PaperTrail
145
136
  #
146
137
  # @api private
147
138
  def changes_in_latest_version
148
- if @in_after_callback && RAILS_GTE_5_1
149
- @record.saved_changes
150
- else
151
- @record.changes
139
+ # Memoized to reduce memory usage
140
+ @changes_in_latest_version ||= begin
141
+ if @in_after_callback && RAILS_GTE_5_1
142
+ @record.saved_changes
143
+ else
144
+ @record.changes
145
+ end
152
146
  end
153
147
  end
154
148
 
@@ -158,7 +152,7 @@ module PaperTrail
158
152
  #
159
153
  # @api private
160
154
  def ignored_attr_has_changed?
161
- ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
155
+ ignored = calculated_ignored_array + @record.paper_trail_options[:skip]
162
156
  ignored.any? && (changed_in_latest_version & ignored).any?
163
157
  end
164
158
 
@@ -220,18 +214,28 @@ module PaperTrail
220
214
  end
221
215
  end
222
216
 
217
+ # @api private
218
+ def notable_changes
219
+ changes_in_latest_version.delete_if { |k, _v|
220
+ !notably_changed.include?(k)
221
+ }
222
+ end
223
+
223
224
  # @api private
224
225
  def notably_changed
225
- only = @record.paper_trail_options[:only].dup
226
- # Remove Hash arguments and then evaluate whether the attributes (the
227
- # keys of the hash) should also get pushed into the collection.
228
- only.delete_if do |obj|
229
- obj.is_a?(Hash) &&
230
- obj.each { |attr, condition|
231
- only << attr if condition.respond_to?(:call) && condition.call(@record)
232
- }
226
+ # Memoized to reduce memory usage
227
+ @notably_changed ||= begin
228
+ only = @record.paper_trail_options[:only].dup
229
+ # Remove Hash arguments and then evaluate whether the attributes (the
230
+ # keys of the hash) should also get pushed into the collection.
231
+ only.delete_if do |obj|
232
+ obj.is_a?(Hash) &&
233
+ obj.each { |attr, condition|
234
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
235
+ }
236
+ end
237
+ only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
233
238
  end
234
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
235
239
  end
236
240
 
237
241
  # Returns hash of attributes (with appropriate attributes serialized),
@@ -239,12 +243,17 @@ module PaperTrail
239
243
  #
240
244
  # @api private
241
245
  def object_attrs_for_paper_trail(is_touch)
242
- attrs = attributes_before_change(is_touch).
243
- except(*@record.paper_trail_options[:skip])
246
+ attrs = nonskipped_attributes_before_change(is_touch)
244
247
  AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
245
248
  attrs
246
249
  end
247
250
 
251
+ # @api private
252
+ def prepare_object_changes(changes)
253
+ changes = serialize_object_changes(changes)
254
+ recordable_object_changes(changes)
255
+ end
256
+
248
257
  # Returns an object which can be assigned to the `object_changes`
249
258
  # attribute of a nascent version record. If the `object_changes` column is
250
259
  # a postgres `json` column, then a hash can be used in the assignment,
@@ -252,9 +261,14 @@ module PaperTrail
252
261
  # serialization here, using `PaperTrail.serializer`.
253
262
  #
254
263
  # @api private
264
+ # @param changes HashWithIndifferentAccess
255
265
  def recordable_object_changes(changes)
256
266
  if PaperTrail.config.object_changes_adapter&.respond_to?(:diff)
257
- changes = PaperTrail.config.object_changes_adapter.diff(changes)
267
+ # We'd like to avoid the `to_hash` here, because it increases memory
268
+ # usage, but that would be a breaking change because
269
+ # `object_changes_adapter` expects a plain `Hash`, not a
270
+ # `HashWithIndifferentAccess`.
271
+ changes = PaperTrail.config.object_changes_adapter.diff(changes.to_hash)
258
272
  end
259
273
 
260
274
  if @record.class.paper_trail.version_class.object_changes_col_is_json?
@@ -293,6 +307,17 @@ module PaperTrail
293
307
  PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch))
294
308
  end
295
309
  end
310
+
311
+ # @api private
312
+ def serialize_object_changes(changes)
313
+ AttributeSerializers::ObjectChangesAttribute.
314
+ new(@record.class).
315
+ serialize(changes)
316
+
317
+ # We'd like to convert this `HashWithIndifferentAccess` to a plain
318
+ # `Hash`, but we don't, to save memory.
319
+ changes
320
+ end
296
321
  end
297
322
  end
298
323
  end
@@ -13,6 +13,7 @@ module PaperTrail
13
13
  # @api private
14
14
  def data
15
15
  data = {
16
+ item: @record,
16
17
  event: @record.paper_trail_event || "create",
17
18
  whodunnit: PaperTrail.request.whodunnit
18
19
  }
@@ -22,13 +22,21 @@ module PaperTrail
22
22
  data[:object] = recordable_object(false)
23
23
  end
24
24
  if record_object_changes?
25
- # Rails' implementation returns nothing on destroy :/
26
- changes = @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h
27
- data[:object_changes] = prepare_object_changes(changes)
25
+ data[:object_changes] = prepare_object_changes(notable_changes)
28
26
  end
29
27
  merge_item_subtype_into(data)
30
28
  merge_metadata_into(data)
31
29
  end
30
+
31
+ private
32
+
33
+ # Rails' implementation (eg. `@record.saved_changes`) returns nothing on
34
+ # destroy, so we have to build the hash we want.
35
+ #
36
+ # @override
37
+ def changes_in_latest_version
38
+ @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h
39
+ end
32
40
  end
33
41
  end
34
42
  end
@@ -25,6 +25,7 @@ module PaperTrail
25
25
  # @api private
26
26
  def data
27
27
  data = {
28
+ item: @record,
28
29
  event: @record.paper_trail_event || "update",
29
30
  whodunnit: PaperTrail.request.whodunnit
30
31
  }
@@ -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
@@ -4,11 +4,42 @@ module PaperTrail
4
4
  module Rails
5
5
  # See http://guides.rubyonrails.org/engines.html
6
6
  class Engine < ::Rails::Engine
7
+ DPR_CONFIG_ENABLED = <<~EOS.squish.freeze
8
+ The rails configuration option config.paper_trail.enabled is deprecated.
9
+ Please use PaperTrail.enabled= instead. People were getting confused
10
+ that PT has both, specifically regarding *when* each was happening. If
11
+ you'd like to keep config.paper_trail, join the discussion at
12
+ https://github.com/paper-trail-gem/paper_trail/pull/1176
13
+ EOS
14
+ private_constant :DPR_CONFIG_ENABLED
15
+ DPR_RUDELY_ENABLING = <<~EOS.squish.freeze
16
+ At some point early in the rails boot process, you have set
17
+ PaperTrail.enabled = false. PT's rails engine is now overriding your
18
+ setting, and setting it to true. We're not sure why, but this is how PT
19
+ has worked since 5.0, when the config.paper_trail.enabled option was
20
+ introduced. This is now deprecated. In the future, PT will not override
21
+ your setting. See
22
+ https://github.com/paper-trail-gem/paper_trail/pull/1176 for discussion.
23
+ EOS
24
+ private_constant :DPR_RUDELY_ENABLING
25
+
7
26
  paths["app/models"] << "lib/paper_trail/frameworks/active_record/models"
27
+
28
+ # --- Begin deprecated section ---
8
29
  config.paper_trail = ActiveSupport::OrderedOptions.new
9
30
  initializer "paper_trail.initialisation" do |app|
10
- PaperTrail.enabled = app.config.paper_trail.fetch(:enabled, true)
31
+ enable = app.config.paper_trail[:enabled]
32
+ if enable.nil?
33
+ unless PaperTrail.enabled?
34
+ ::ActiveSupport::Deprecation.warn(DPR_RUDELY_ENABLING)
35
+ PaperTrail.enabled = true
36
+ end
37
+ else
38
+ ::ActiveSupport::Deprecation.warn(DPR_CONFIG_ENABLED)
39
+ PaperTrail.enabled = enable
40
+ end
11
41
  end
42
+ # --- End deprecated section ---
12
43
  end
13
44
  end
14
45
  end
@@ -18,6 +18,11 @@ module PaperTrail
18
18
  `abstract_class`. This is fine, but all application models must be
19
19
  configured to use concrete (not abstract) version models.
20
20
  STR
21
+ E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze
22
+ To use PaperTrail's per-model limit in your %s model, you must have an
23
+ item_subtype column in your versions table. See documentation sections
24
+ 2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column.
25
+ STR
21
26
  DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
22
27
  Passing versions association name as `has_paper_trail versions: %{versions_name}`
23
28
  is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
@@ -112,6 +117,7 @@ module PaperTrail
112
117
  @model_class.send :include, ::PaperTrail::Model::InstanceMethods
113
118
  setup_options(options)
114
119
  setup_associations(options)
120
+ check_presence_of_item_subtype_column(options)
115
121
  @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
116
122
  setup_callbacks_from_options options[:on]
117
123
  end
@@ -122,10 +128,6 @@ module PaperTrail
122
128
 
123
129
  private
124
130
 
125
- def active_record_gem_version
126
- Gem::Version.new(ActiveRecord::VERSION::STRING)
127
- end
128
-
129
131
  # Raises an error if the provided class is an `abstract_class`.
130
132
  # @api private
131
133
  def assert_concrete_activerecord_class(class_name)
@@ -135,8 +137,17 @@ module PaperTrail
135
137
  end
136
138
 
137
139
  def cannot_record_after_destroy?
138
- Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
139
- ::ActiveRecord::Base.belongs_to_required_by_default
140
+ ::ActiveRecord::Base.belongs_to_required_by_default
141
+ end
142
+
143
+ # Some options require the presence of the `item_subtype` column. Currently
144
+ # only `limit`, but in the future there may be others.
145
+ #
146
+ # @api private
147
+ def check_presence_of_item_subtype_column(options)
148
+ return unless options.key?(:limit)
149
+ return if version_class.item_subtype_column_present?
150
+ raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name)
140
151
  end
141
152
 
142
153
  def check_version_class_name(options)
@@ -67,14 +67,14 @@ module PaperTrail
67
67
 
68
68
  def record_create
69
69
  return unless enabled?
70
- event = Events::Create.new(@record, true)
71
70
 
72
- # Merge data from `Event` with data from PT-AT. We no longer use
73
- # `data_for_create` but PT-AT still does.
74
- data = event.data.merge(data_for_create)
75
-
76
- versions_assoc = @record.send(@record.class.versions_association_name)
77
- versions_assoc.create!(data)
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
77
+ end
78
78
  end
79
79
 
80
80
  # PT-AT extends this method to add its transaction id.
@@ -119,19 +119,22 @@ module PaperTrail
119
119
  # paper_trail-association_tracking
120
120
  def record_update(force:, in_after_callback:, is_touch:)
121
121
  return unless enabled?
122
- event = Events::Update.new(@record, in_after_callback, is_touch, nil)
123
- return unless force || event.changed_notably?
124
122
 
125
- # Merge data from `Event` with data from PT-AT. We no longer use
126
- # `data_for_update` but PT-AT still does.
127
- data = event.data.merge(data_for_update)
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
128
129
 
129
- versions_assoc = @record.send(@record.class.versions_association_name)
130
- version = versions_assoc.create(data)
131
- if version.errors.any?
132
- log_version_errors(version, :update)
133
- else
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
134
135
  version
136
+ else
137
+ log_version_errors(version, :update)
135
138
  end
136
139
  end
137
140
 
@@ -250,6 +253,35 @@ module PaperTrail
250
253
  @record.send(@record.class.versions_association_name).reset
251
254
  end
252
255
 
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)
266
+ end
267
+
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)
277
+
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)
283
+ end
284
+
253
285
  def log_version_errors(version, action)
254
286
  version.logger&.warn(
255
287
  "Unable to create version for #{action} of #{@record.class.name}" \
@@ -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)
@@ -120,6 +118,7 @@ module PaperTrail
120
118
  # this method returns the constant `Animal`. You can see this particular
121
119
  # example in action in `spec/models/animal_spec.rb`.
122
120
  #
121
+ # TODO: Duplication: similar `constantize` in VersionConcern#version_limit
123
122
  def version_reification_class(version, attrs)
124
123
  inheritance_column_name = version.item_type.constantize.inheritance_column
125
124
  inher_col_value = attrs[inheritance_column_name]
@@ -12,7 +12,12 @@ module PaperTrail
12
12
  ::YAML.load string
13
13
  end
14
14
 
15
+ # @param object (Hash | HashWithIndifferentAccess) - Coming from
16
+ # `recordable_object` `object` will be a plain `Hash`. However, due to
17
+ # recent [memory optimizations](https://git.io/fjeYv), when coming from
18
+ # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
15
19
  def dump(object)
20
+ object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
16
21
  ::YAML.dump object
17
22
  end
18
23
 
@@ -25,6 +25,10 @@ module PaperTrail
25
25
 
26
26
  # :nodoc:
27
27
  module ClassMethods
28
+ def item_subtype_column_present?
29
+ column_names.include?("item_subtype")
30
+ end
31
+
28
32
  def with_item_keys(item_type, item_id)
29
33
  where item_type: item_type, item_id: item_id
30
34
  end
@@ -141,6 +145,7 @@ module PaperTrail
141
145
  # Default: false.
142
146
  # @return `ActiveRecord::Relation`
143
147
  # @api public
148
+ # rubocop:disable Style/OptionalBooleanParameter
144
149
  def preceding(obj, timestamp_arg = false)
145
150
  if timestamp_arg != true && primary_key_is_int?
146
151
  preceding_by_id(obj)
@@ -148,6 +153,7 @@ module PaperTrail
148
153
  preceding_by_timestamp(obj)
149
154
  end
150
155
  end
156
+ # rubocop:enable Style/OptionalBooleanParameter
151
157
 
152
158
  # Returns versions after `obj`.
153
159
  #
@@ -156,6 +162,7 @@ module PaperTrail
156
162
  # Default: false.
157
163
  # @return `ActiveRecord::Relation`
158
164
  # @api public
165
+ # rubocop:disable Style/OptionalBooleanParameter
159
166
  def subsequent(obj, timestamp_arg = false)
160
167
  if timestamp_arg != true && primary_key_is_int?
161
168
  subsequent_by_id(obj)
@@ -163,6 +170,7 @@ module PaperTrail
163
170
  subsequent_by_timestamp(obj)
164
171
  end
165
172
  end
173
+ # rubocop:enable Style/OptionalBooleanParameter
166
174
 
167
175
  private
168
176
 
@@ -201,18 +209,8 @@ module PaperTrail
201
209
 
202
210
  # Restore the item from this version.
203
211
  #
204
- # Optionally this can also restore all :has_one and :has_many (including
205
- # has_many :through) associations as they were "at the time", if they are
206
- # also being versioned by PaperTrail.
207
- #
208
212
  # Options:
209
213
  #
210
- # - :has_one
211
- # - `true` - Also reify has_one associations.
212
- # - `false - Default.
213
- # - :has_many
214
- # - `true` - Also reify has_many and has_many :through associations.
215
- # - `false` - Default.
216
214
  # - :mark_for_destruction
217
215
  # - `true` - Mark the has_one/has_many associations that did not exist in
218
216
  # the reified version for destruction, instead of removing them.
@@ -254,13 +252,6 @@ module PaperTrail
254
252
  end
255
253
  alias version_author terminator
256
254
 
257
- def sibling_versions(reload = false)
258
- if reload || !defined?(@sibling_versions) || @sibling_versions.nil?
259
- @sibling_versions = self.class.with_item_keys(item_type, item_id)
260
- end
261
- @sibling_versions
262
- end
263
-
264
255
  def next
265
256
  @next ||= sibling_versions.subsequent(self).first
266
257
  end
@@ -270,8 +261,9 @@ module PaperTrail
270
261
  end
271
262
 
272
263
  # Returns an integer representing the chronological position of the
273
- # version among its siblings (see `sibling_versions`). The "create" event,
274
- # for example, has an index of 0.
264
+ # version among its siblings. The "create" event, for example, has an index
265
+ # of 0.
266
+ #
275
267
  # @api public
276
268
  def index
277
269
  @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
@@ -329,7 +321,7 @@ module PaperTrail
329
321
  # Enforces the `version_limit`, if set. Default: no limit.
330
322
  # @api private
331
323
  def enforce_version_limit!
332
- limit = PaperTrail.config.version_limit
324
+ limit = version_limit
333
325
  return unless limit.is_a? Numeric
334
326
  previous_versions = sibling_versions.not_creates.
335
327
  order(self.class.timestamp_sort_order("asc"))
@@ -337,5 +329,26 @@ module PaperTrail
337
329
  excess_versions = previous_versions - previous_versions.last(limit)
338
330
  excess_versions.map(&:destroy)
339
331
  end
332
+
333
+ # @api private
334
+ def sibling_versions
335
+ @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
336
+ end
337
+
338
+ # See docs section 2.e. Limiting the Number of Versions Created.
339
+ # The version limit can be global or per-model.
340
+ #
341
+ # @api private
342
+ #
343
+ # TODO: Duplication: similar `constantize` in Reifier#version_reification_class
344
+ def version_limit
345
+ if self.class.item_subtype_column_present?
346
+ klass = (item_subtype || item_type).constantize
347
+ if klass&.paper_trail_options&.key?(:limit)
348
+ return klass.paper_trail_options[:limit]
349
+ end
350
+ end
351
+ PaperTrail.config.version_limit
352
+ end
340
353
  end
341
354
  end
@@ -7,8 +7,8 @@ module PaperTrail
7
7
  # because of this confusion, but it's not worth the breaking change.
8
8
  # People are encouraged to use `PaperTrail.gem_version` instead.
9
9
  module VERSION
10
- MAJOR = 10
11
- MINOR = 1
10
+ MAJOR = 11
11
+ MINOR = 0
12
12
  TINY = 0
13
13
 
14
14
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_trail
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.1.0
4
+ version: 11.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Stewart
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-12-04 00:00:00.000000000 Z
13
+ date: 2020-08-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -18,20 +18,14 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '4.2'
22
- - - "<"
23
- - !ruby/object:Gem::Version
24
- version: '6.0'
21
+ version: '5.2'
25
22
  type: :runtime
26
23
  prerelease: false
27
24
  version_requirements: !ruby/object:Gem::Requirement
28
25
  requirements:
29
26
  - - ">="
30
27
  - !ruby/object:Gem::Version
31
- version: '4.2'
32
- - - "<"
33
- - !ruby/object:Gem::Version
34
- version: '6.0'
28
+ version: '5.2'
35
29
  - !ruby/object:Gem::Dependency
36
30
  name: request_store
37
31
  requirement: !ruby/object:Gem::Requirement
@@ -66,28 +60,28 @@ dependencies:
66
60
  requirements:
67
61
  - - "~>"
68
62
  - !ruby/object:Gem::Version
69
- version: '10.0'
63
+ version: '11.0'
70
64
  type: :development
71
65
  prerelease: false
72
66
  version_requirements: !ruby/object:Gem::Requirement
73
67
  requirements:
74
68
  - - "~>"
75
69
  - !ruby/object:Gem::Version
76
- version: '10.0'
70
+ version: '11.0'
77
71
  - !ruby/object:Gem::Dependency
78
72
  name: ffaker
79
73
  requirement: !ruby/object:Gem::Requirement
80
74
  requirements:
81
75
  - - "~>"
82
76
  - !ruby/object:Gem::Version
83
- version: '2.8'
77
+ version: '2.11'
84
78
  type: :development
85
79
  prerelease: false
86
80
  version_requirements: !ruby/object:Gem::Requirement
87
81
  requirements:
88
82
  - - "~>"
89
83
  - !ruby/object:Gem::Version
90
- version: '2.8'
84
+ version: '2.11'
91
85
  - !ruby/object:Gem::Dependency
92
86
  name: generator_spec
93
87
  requirement: !ruby/object:Gem::Requirement
@@ -103,117 +97,137 @@ dependencies:
103
97
  - !ruby/object:Gem::Version
104
98
  version: 0.9.4
105
99
  - !ruby/object:Gem::Dependency
106
- name: mysql2
100
+ name: memory_profiler
107
101
  requirement: !ruby/object:Gem::Requirement
108
102
  requirements:
109
103
  - - "~>"
110
104
  - !ruby/object:Gem::Version
111
- version: 0.5.2
105
+ version: 0.9.14
112
106
  type: :development
113
107
  prerelease: false
114
108
  version_requirements: !ruby/object:Gem::Requirement
115
109
  requirements:
116
110
  - - "~>"
117
111
  - !ruby/object:Gem::Version
118
- version: 0.5.2
112
+ version: 0.9.14
119
113
  - !ruby/object:Gem::Dependency
120
- name: paper_trail-association_tracking
114
+ name: rake
121
115
  requirement: !ruby/object:Gem::Requirement
122
116
  requirements:
123
- - - "<"
117
+ - - "~>"
124
118
  - !ruby/object:Gem::Version
125
- version: '2'
119
+ version: '13.0'
126
120
  type: :development
127
121
  prerelease: false
128
122
  version_requirements: !ruby/object:Gem::Requirement
129
123
  requirements:
130
- - - "<"
124
+ - - "~>"
131
125
  - !ruby/object:Gem::Version
132
- version: '2'
126
+ version: '13.0'
133
127
  - !ruby/object:Gem::Dependency
134
- name: pg
128
+ name: rspec-rails
135
129
  requirement: !ruby/object:Gem::Requirement
136
130
  requirements:
137
131
  - - "~>"
138
132
  - !ruby/object:Gem::Version
139
- version: '1.0'
133
+ version: '4.0'
140
134
  type: :development
141
135
  prerelease: false
142
136
  version_requirements: !ruby/object:Gem::Requirement
143
137
  requirements:
144
138
  - - "~>"
145
139
  - !ruby/object:Gem::Version
146
- version: '1.0'
140
+ version: '4.0'
147
141
  - !ruby/object:Gem::Dependency
148
- name: rake
142
+ name: rubocop
149
143
  requirement: !ruby/object:Gem::Requirement
150
144
  requirements:
151
145
  - - "~>"
152
146
  - !ruby/object:Gem::Version
153
- version: '12.3'
147
+ version: 0.89.1
154
148
  type: :development
155
149
  prerelease: false
156
150
  version_requirements: !ruby/object:Gem::Requirement
157
151
  requirements:
158
152
  - - "~>"
159
153
  - !ruby/object:Gem::Version
160
- version: '12.3'
154
+ version: 0.89.1
161
155
  - !ruby/object:Gem::Dependency
162
- name: rspec-rails
156
+ name: rubocop-performance
163
157
  requirement: !ruby/object:Gem::Requirement
164
158
  requirements:
165
159
  - - "~>"
166
160
  - !ruby/object:Gem::Version
167
- version: '3.8'
161
+ version: 1.7.1
168
162
  type: :development
169
163
  prerelease: false
170
164
  version_requirements: !ruby/object:Gem::Requirement
171
165
  requirements:
172
166
  - - "~>"
173
167
  - !ruby/object:Gem::Version
174
- version: '3.8'
168
+ version: 1.7.1
175
169
  - !ruby/object:Gem::Dependency
176
- name: rubocop
170
+ name: rubocop-rspec
177
171
  requirement: !ruby/object:Gem::Requirement
178
172
  requirements:
179
173
  - - "~>"
180
174
  - !ruby/object:Gem::Version
181
- version: 0.58.2
175
+ version: 1.42.0
182
176
  type: :development
183
177
  prerelease: false
184
178
  version_requirements: !ruby/object:Gem::Requirement
185
179
  requirements:
186
180
  - - "~>"
187
181
  - !ruby/object:Gem::Version
188
- version: 0.58.2
182
+ version: 1.42.0
189
183
  - !ruby/object:Gem::Dependency
190
- name: rubocop-rspec
184
+ name: mysql2
191
185
  requirement: !ruby/object:Gem::Requirement
192
186
  requirements:
193
187
  - - "~>"
194
188
  - !ruby/object:Gem::Version
195
- version: 1.28.0
189
+ version: '0.5'
196
190
  type: :development
197
191
  prerelease: false
198
192
  version_requirements: !ruby/object:Gem::Requirement
199
193
  requirements:
200
194
  - - "~>"
201
195
  - !ruby/object:Gem::Version
202
- version: 1.28.0
196
+ version: '0.5'
197
+ - !ruby/object:Gem::Dependency
198
+ name: pg
199
+ requirement: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: '0.18'
204
+ - - "<"
205
+ - !ruby/object:Gem::Version
206
+ version: '2.0'
207
+ type: :development
208
+ prerelease: false
209
+ version_requirements: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0.18'
214
+ - - "<"
215
+ - !ruby/object:Gem::Version
216
+ version: '2.0'
203
217
  - !ruby/object:Gem::Dependency
204
218
  name: sqlite3
205
219
  requirement: !ruby/object:Gem::Requirement
206
220
  requirements:
207
221
  - - "~>"
208
222
  - !ruby/object:Gem::Version
209
- version: '1.3'
223
+ version: '1.4'
210
224
  type: :development
211
225
  prerelease: false
212
226
  version_requirements: !ruby/object:Gem::Requirement
213
227
  requirements:
214
228
  - - "~>"
215
229
  - !ruby/object:Gem::Version
216
- version: '1.3'
230
+ version: '1.4'
217
231
  description: |
218
232
  Track changes to your models, for auditing or versioning. See how a model looked
219
233
  at any stage in its lifecycle, revert it to any version, or restore it after it
@@ -227,7 +241,6 @@ files:
227
241
  - lib/generators/paper_trail/install/install_generator.rb
228
242
  - lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb
229
243
  - lib/generators/paper_trail/install/templates/create_versions.rb.erb
230
- - lib/generators/paper_trail/install_generator.rb
231
244
  - lib/generators/paper_trail/migration_generator.rb
232
245
  - lib/generators/paper_trail/update_item_subtype/USAGE
233
246
  - lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb
@@ -239,6 +252,7 @@ files:
239
252
  - lib/paper_trail/attribute_serializers/object_attribute.rb
240
253
  - lib/paper_trail/attribute_serializers/object_changes_attribute.rb
241
254
  - lib/paper_trail/cleaner.rb
255
+ - lib/paper_trail/compatibility.rb
242
256
  - lib/paper_trail/config.rb
243
257
  - lib/paper_trail/events/base.rb
244
258
  - lib/paper_trail/events/create.rb
@@ -277,15 +291,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
277
291
  requirements:
278
292
  - - ">="
279
293
  - !ruby/object:Gem::Version
280
- version: 2.3.0
294
+ version: 2.4.0
281
295
  required_rubygems_version: !ruby/object:Gem::Requirement
282
296
  requirements:
283
297
  - - ">="
284
298
  - !ruby/object:Gem::Version
285
299
  version: 1.3.6
286
300
  requirements: []
287
- rubyforge_project:
288
- rubygems_version: 2.6.14.3
301
+ rubygems_version: 3.0.3
289
302
  signing_key:
290
303
  specification_version: 4
291
304
  summary: Track changes to your models.
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module PaperTrail
7
- # Installs PaperTrail in a rails app.
8
- class InstallGenerator < ::Rails::Generators::Base
9
- include ::Rails::Generators::Migration
10
-
11
- # Class names of MySQL adapters.
12
- # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
13
- # - `Mysql2Adapter` - Used by `mysql2` gem.
14
- MYSQL_ADAPTERS = [
15
- "ActiveRecord::ConnectionAdapters::MysqlAdapter",
16
- "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
17
- ].freeze
18
-
19
- source_root File.expand_path("templates", __dir__)
20
- class_option(
21
- :with_changes,
22
- type: :boolean,
23
- default: false,
24
- desc: "Store changeset (diff) with each version"
25
- )
26
-
27
- desc "Generates (but does not run) a migration to add a versions table."
28
-
29
- def create_migration_file
30
- add_paper_trail_migration("create_versions")
31
- add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes?
32
- end
33
-
34
- def self.next_migration_number(dirname)
35
- ::ActiveRecord::Generators::Base.next_migration_number(dirname)
36
- end
37
-
38
- protected
39
-
40
- def add_paper_trail_migration(template)
41
- migration_dir = File.expand_path("db/migrate")
42
- if self.class.migration_exists?(migration_dir, template)
43
- ::Kernel.warn "Migration already exists: #{template}"
44
- else
45
- migration_template(
46
- "#{template}.rb.erb",
47
- "db/migrate/#{template}.rb",
48
- item_type_options: item_type_options,
49
- migration_version: migration_version,
50
- versions_table_options: versions_table_options
51
- )
52
- end
53
- end
54
-
55
- private
56
-
57
- # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
58
- # See https://github.com/paper-trail-gem/paper_trail/issues/651
59
- def item_type_options
60
- opt = { null: false }
61
- opt[:limit] = 191 if mysql?
62
- ", #{opt}"
63
- end
64
-
65
- def migration_version
66
- major = ActiveRecord::VERSION::MAJOR
67
- if major >= 5
68
- "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
69
- end
70
- end
71
-
72
- def mysql?
73
- MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name)
74
- end
75
-
76
- # Even modern versions of MySQL still use `latin1` as the default character
77
- # encoding. Many users are not aware of this, and run into trouble when they
78
- # try to use PaperTrail in apps that otherwise tend to use UTF-8. Postgres, by
79
- # comparison, uses UTF-8 except in the unusual case where the OS is configured
80
- # with a custom locale.
81
- #
82
- # - https://dev.mysql.com/doc/refman/5.7/en/charset-applications.html
83
- # - http://www.postgresql.org/docs/9.4/static/multibyte.html
84
- #
85
- # Furthermore, MySQL's original implementation of UTF-8 was flawed, and had
86
- # to be fixed later by introducing a new charset, `utf8mb4`.
87
- #
88
- # - https://mathiasbynens.be/notes/mysql-utf8mb4
89
- # - https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
90
- #
91
- def versions_table_options
92
- if mysql?
93
- ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }'
94
- else
95
- ""
96
- end
97
- end
98
- end
99
- end