paper_trail 10.1.0 → 11.0.0

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