paper_trail 5.2.3 → 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.
Files changed (198) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install/USAGE +3 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +75 -0
  4. data/lib/generators/paper_trail/{templates/add_object_changes_to_versions.rb → install/templates/add_object_changes_to_versions.rb.erb} +1 -1
  5. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
  10. data/lib/paper_trail.rb +82 -130
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +15 -44
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
  15. data/lib/paper_trail/cleaner.rb +3 -1
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +11 -49
  18. data/lib/paper_trail/events/base.rb +323 -0
  19. data/lib/paper_trail/events/create.rb +32 -0
  20. data/lib/paper_trail/events/destroy.rb +42 -0
  21. data/lib/paper_trail/events/update.rb +60 -0
  22. data/lib/paper_trail/frameworks/active_record.rb +2 -1
  23. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +8 -3
  24. data/lib/paper_trail/frameworks/cucumber.rb +5 -3
  25. data/lib/paper_trail/frameworks/rails.rb +2 -0
  26. data/lib/paper_trail/frameworks/rails/controller.rb +33 -43
  27. data/lib/paper_trail/frameworks/rails/engine.rb +34 -1
  28. data/lib/paper_trail/frameworks/rspec.rb +17 -4
  29. data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
  30. data/lib/paper_trail/has_paper_trail.rb +22 -310
  31. data/lib/paper_trail/model_config.rb +157 -109
  32. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  33. data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
  34. data/lib/paper_trail/record_history.rb +3 -9
  35. data/lib/paper_trail/record_trail.rb +169 -319
  36. data/lib/paper_trail/reifier.rb +53 -374
  37. data/lib/paper_trail/request.rb +166 -0
  38. data/lib/paper_trail/serializers/json.rb +9 -10
  39. data/lib/paper_trail/serializers/yaml.rb +15 -28
  40. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
  41. data/lib/paper_trail/version_concern.rb +160 -155
  42. data/lib/paper_trail/version_number.rb +12 -4
  43. metadata +77 -372
  44. data/.github/CONTRIBUTING.md +0 -109
  45. data/.github/ISSUE_TEMPLATE.md +0 -13
  46. data/.gitignore +0 -23
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -99
  49. data/.rubocop_todo.yml +0 -22
  50. data/.travis.yml +0 -41
  51. data/Appraisals +0 -38
  52. data/CHANGELOG.md +0 -560
  53. data/Gemfile +0 -2
  54. data/MIT-LICENSE +0 -20
  55. data/README.md +0 -1654
  56. data/Rakefile +0 -30
  57. data/doc/bug_report_template.rb +0 -69
  58. data/doc/warning_about_not_setting_whodunnit.md +0 -32
  59. data/gemfiles/ar3.gemfile +0 -19
  60. data/gemfiles/ar4.gemfile +0 -8
  61. data/gemfiles/ar5.gemfile +0 -9
  62. data/lib/generators/paper_trail/USAGE +0 -2
  63. data/lib/generators/paper_trail/default_initializer.rb +0 -0
  64. data/lib/generators/paper_trail/install_generator.rb +0 -57
  65. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -13
  66. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -22
  67. data/lib/generators/paper_trail/templates/create_versions.rb +0 -80
  68. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
  69. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -11
  70. data/lib/paper_trail/frameworks/sinatra.rb +0 -40
  71. data/lib/paper_trail/version_association_concern.rb +0 -17
  72. data/paper_trail.gemspec +0 -56
  73. data/spec/generators/install_generator_spec.rb +0 -66
  74. data/spec/generators/paper_trail/templates/create_versions_spec.rb +0 -51
  75. data/spec/models/animal_spec.rb +0 -36
  76. data/spec/models/boolit_spec.rb +0 -48
  77. data/spec/models/callback_modifier_spec.rb +0 -96
  78. data/spec/models/car_spec.rb +0 -13
  79. data/spec/models/custom_primary_key_record_spec.rb +0 -18
  80. data/spec/models/fluxor_spec.rb +0 -17
  81. data/spec/models/gadget_spec.rb +0 -68
  82. data/spec/models/joined_version_spec.rb +0 -47
  83. data/spec/models/json_version_spec.rb +0 -102
  84. data/spec/models/kitchen/banana_spec.rb +0 -14
  85. data/spec/models/not_on_update_spec.rb +0 -22
  86. data/spec/models/post_with_status_spec.rb +0 -50
  87. data/spec/models/skipper_spec.rb +0 -46
  88. data/spec/models/thing_spec.rb +0 -11
  89. data/spec/models/truck_spec.rb +0 -5
  90. data/spec/models/vehicle_spec.rb +0 -5
  91. data/spec/models/version_spec.rb +0 -272
  92. data/spec/models/widget_spec.rb +0 -343
  93. data/spec/modules/paper_trail_spec.rb +0 -27
  94. data/spec/modules/version_concern_spec.rb +0 -31
  95. data/spec/modules/version_number_spec.rb +0 -43
  96. data/spec/paper_trail/config_spec.rb +0 -33
  97. data/spec/paper_trail_spec.rb +0 -79
  98. data/spec/rails_helper.rb +0 -34
  99. data/spec/requests/articles_spec.rb +0 -34
  100. data/spec/spec_helper.rb +0 -114
  101. data/spec/support/alt_db_init.rb +0 -54
  102. data/test/custom_json_serializer.rb +0 -13
  103. data/test/dummy/Rakefile +0 -7
  104. data/test/dummy/app/controllers/application_controller.rb +0 -33
  105. data/test/dummy/app/controllers/articles_controller.rb +0 -20
  106. data/test/dummy/app/controllers/test_controller.rb +0 -5
  107. data/test/dummy/app/controllers/widgets_controller.rb +0 -32
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/models/animal.rb +0 -6
  110. data/test/dummy/app/models/article.rb +0 -24
  111. data/test/dummy/app/models/authorship.rb +0 -5
  112. data/test/dummy/app/models/bar_habtm.rb +0 -4
  113. data/test/dummy/app/models/book.rb +0 -9
  114. data/test/dummy/app/models/boolit.rb +0 -4
  115. data/test/dummy/app/models/callback_modifier.rb +0 -45
  116. data/test/dummy/app/models/car.rb +0 -3
  117. data/test/dummy/app/models/cat.rb +0 -2
  118. data/test/dummy/app/models/chapter.rb +0 -9
  119. data/test/dummy/app/models/citation.rb +0 -5
  120. data/test/dummy/app/models/custom_primary_key_record.rb +0 -13
  121. data/test/dummy/app/models/customer.rb +0 -4
  122. data/test/dummy/app/models/document.rb +0 -4
  123. data/test/dummy/app/models/dog.rb +0 -2
  124. data/test/dummy/app/models/editor.rb +0 -4
  125. data/test/dummy/app/models/editorship.rb +0 -5
  126. data/test/dummy/app/models/elephant.rb +0 -3
  127. data/test/dummy/app/models/fluxor.rb +0 -3
  128. data/test/dummy/app/models/foo_habtm.rb +0 -5
  129. data/test/dummy/app/models/foo_widget.rb +0 -2
  130. data/test/dummy/app/models/fruit.rb +0 -5
  131. data/test/dummy/app/models/gadget.rb +0 -3
  132. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  133. data/test/dummy/app/models/legacy_widget.rb +0 -4
  134. data/test/dummy/app/models/line_item.rb +0 -4
  135. data/test/dummy/app/models/not_on_update.rb +0 -4
  136. data/test/dummy/app/models/order.rb +0 -5
  137. data/test/dummy/app/models/paragraph.rb +0 -5
  138. data/test/dummy/app/models/person.rb +0 -38
  139. data/test/dummy/app/models/post.rb +0 -3
  140. data/test/dummy/app/models/post_with_status.rb +0 -8
  141. data/test/dummy/app/models/protected_widget.rb +0 -3
  142. data/test/dummy/app/models/quotation.rb +0 -5
  143. data/test/dummy/app/models/section.rb +0 -6
  144. data/test/dummy/app/models/skipper.rb +0 -6
  145. data/test/dummy/app/models/song.rb +0 -41
  146. data/test/dummy/app/models/thing.rb +0 -3
  147. data/test/dummy/app/models/translation.rb +0 -4
  148. data/test/dummy/app/models/truck.rb +0 -4
  149. data/test/dummy/app/models/vehicle.rb +0 -4
  150. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  151. data/test/dummy/app/models/widget.rb +0 -16
  152. data/test/dummy/app/models/wotsit.rb +0 -8
  153. data/test/dummy/app/versions/custom_primary_key_record_version.rb +0 -3
  154. data/test/dummy/app/versions/joined_version.rb +0 -6
  155. data/test/dummy/app/versions/json_version.rb +0 -3
  156. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  157. data/test/dummy/app/versions/post_version.rb +0 -3
  158. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  159. data/test/dummy/config.ru +0 -4
  160. data/test/dummy/config/application.rb +0 -80
  161. data/test/dummy/config/boot.rb +0 -10
  162. data/test/dummy/config/database.mysql.yml +0 -19
  163. data/test/dummy/config/database.postgres.yml +0 -15
  164. data/test/dummy/config/database.sqlite.yml +0 -15
  165. data/test/dummy/config/environment.rb +0 -5
  166. data/test/dummy/config/environments/development.rb +0 -41
  167. data/test/dummy/config/environments/production.rb +0 -74
  168. data/test/dummy/config/environments/test.rb +0 -51
  169. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -9
  170. data/test/dummy/config/initializers/inflections.rb +0 -10
  171. data/test/dummy/config/initializers/mime_types.rb +0 -5
  172. data/test/dummy/config/initializers/paper_trail.rb +0 -9
  173. data/test/dummy/config/initializers/secret_token.rb +0 -9
  174. data/test/dummy/config/initializers/session_store.rb +0 -8
  175. data/test/dummy/config/locales/en.yml +0 -5
  176. data/test/dummy/config/routes.rb +0 -4
  177. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -361
  178. data/test/dummy/db/schema.rb +0 -288
  179. data/test/dummy/script/rails +0 -8
  180. data/test/functional/controller_test.rb +0 -90
  181. data/test/functional/enabled_for_controller_test.rb +0 -28
  182. data/test/functional/modular_sinatra_test.rb +0 -46
  183. data/test/functional/sinatra_test.rb +0 -51
  184. data/test/functional/thread_safety_test.rb +0 -46
  185. data/test/test_helper.rb +0 -127
  186. data/test/time_travel_helper.rb +0 -1
  187. data/test/unit/associations_test.rb +0 -1016
  188. data/test/unit/cleaner_test.rb +0 -188
  189. data/test/unit/inheritance_column_test.rb +0 -43
  190. data/test/unit/model_test.rb +0 -1489
  191. data/test/unit/protected_attrs_test.rb +0 -52
  192. data/test/unit/serializer_test.rb +0 -119
  193. data/test/unit/serializers/json_test.rb +0 -95
  194. data/test/unit/serializers/mixin_json_test.rb +0 -37
  195. data/test/unit/serializers/mixin_yaml_test.rb +0 -53
  196. data/test/unit/serializers/yaml_test.rb +0 -54
  197. data/test/unit/timestamp_test.rb +0 -41
  198. data/test/unit/version_test.rb +0 -119
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/type_serializers/postgres_array_serializer"
4
+
5
+ module PaperTrail
6
+ module AttributeSerializers
7
+ # Values returned by some Active Record serializers are
8
+ # not suited for writing JSON to a text column. This factory
9
+ # replaces certain default Active Record serializers
10
+ # with custom PaperTrail ones.
11
+ module AttributeSerializerFactory
12
+ AR_PG_ARRAY_CLASS = "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array"
13
+
14
+ def self.for(klass, attr)
15
+ active_record_serializer = klass.type_for_attribute(attr)
16
+ if active_record_serializer.class.name == AR_PG_ARRAY_CLASS
17
+ TypeSerializers::PostgresArraySerializer.new(
18
+ active_record_serializer.subtype,
19
+ active_record_serializer.delimiter
20
+ )
21
+ else
22
+ active_record_serializer
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/attribute_serializers/attribute_serializer_factory"
4
+
1
5
  module PaperTrail
2
6
  # :nodoc:
3
7
  module AttributeSerializers
@@ -6,8 +10,7 @@ module PaperTrail
6
10
  # to an attribute of type `ActiveRecord::Type::Integer`.
7
11
  #
8
12
  # This implementation depends on the `type_for_attribute` method, which was
9
- # introduced in rails 4.2. In older versions of rails, we shim this method
10
- # with `LegacyActiveRecordShim`.
13
+ # introduced in rails 4.2. As of PT 8, we no longer support rails < 4.2.
11
14
  class CastAttributeSerializer
12
15
  def initialize(klass)
13
16
  @klass = klass
@@ -29,50 +32,18 @@ module PaperTrail
29
32
  end
30
33
  end
31
34
 
32
- if ::ActiveRecord::VERSION::MAJOR >= 5
33
- # This implementation uses AR 5's `serialize` and `deserialize`.
34
- class CastAttributeSerializer
35
- def serialize(attr, val)
36
- @klass.type_for_attribute(attr).serialize(val)
37
- end
38
-
39
- def deserialize(attr, val)
40
- if defined_enums[attr] && val.is_a?(::String)
41
- # Because PT 4 used to save the string version of enums to `object_changes`
42
- val
43
- else
44
- @klass.type_for_attribute(attr).deserialize(val)
45
- end
46
- end
35
+ # Uses AR 5's `serialize` and `deserialize`.
36
+ class CastAttributeSerializer
37
+ def serialize(attr, val)
38
+ AttributeSerializerFactory.for(@klass, attr).serialize(val)
47
39
  end
48
- else
49
- # This implementation uses AR 4.2's `type_cast_for_database`. For
50
- # versions of AR < 4.2 we provide an implementation of
51
- # `type_cast_for_database` in our shim attribute type classes,
52
- # `NoOpAttribute` and `SerializedAttribute`.
53
- class CastAttributeSerializer
54
- def serialize(attr, val)
55
- castable_val = val
56
- if defined_enums[attr]
57
- # `attr` is an enum. Find the number that corresponds to `val`. If `val` is
58
- # a number already, there won't be a corresponding entry, just use `val`.
59
- castable_val = defined_enums[attr][val] || val
60
- end
61
- @klass.type_for_attribute(attr).type_cast_for_database(castable_val)
62
- end
63
40
 
64
- def deserialize(attr, val)
65
- if defined_enums[attr] && val.is_a?(::String)
66
- # Because PT 4 used to save the string version of enums to `object_changes`
67
- val
68
- else
69
- val = @klass.type_for_attribute(attr).type_cast_from_database(val)
70
- if defined_enums[attr]
71
- defined_enums[attr].key(val)
72
- else
73
- val
74
- end
75
- 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)
76
47
  end
77
48
  end
78
49
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/attribute_serializers/cast_attribute_serializer"
2
4
 
3
5
  module PaperTrail
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/attribute_serializers/cast_attribute_serializer"
2
4
 
3
5
  module PaperTrail
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  # Utilities for deleting version records.
3
5
  module Cleaner
@@ -52,7 +54,7 @@ module PaperTrail
52
54
  # versions.
53
55
  # @api private
54
56
  def group_versions_by_date(versions)
55
- versions.group_by { |v| v.send(PaperTrail.timestamp_field).to_date }
57
+ versions.group_by { |v| v.created_at.to_date }
56
58
  end
57
59
  end
58
60
  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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "singleton"
2
4
  require "paper_trail/serializers/yaml"
3
5
 
@@ -6,9 +8,14 @@ module PaperTrail
6
8
  # configuration can be found in `paper_trail.rb`, others in `controller.rb`.
7
9
  class Config
8
10
  include Singleton
9
- attr_accessor :serializer, :version_limit
10
- attr_reader :timestamp_field # deprecated
11
- attr_writer :track_associations
11
+
12
+ attr_accessor(
13
+ :association_reify_error_behaviour,
14
+ :object_changes_adapter,
15
+ :serializer,
16
+ :version_limit,
17
+ :has_paper_trail_defaults
18
+ )
12
19
 
13
20
  def initialize
14
21
  # Variables which affect all threads, whose access is synchronized.
@@ -16,53 +23,8 @@ module PaperTrail
16
23
  @enabled = true
17
24
 
18
25
  # Variables which affect all threads, whose access is *not* synchronized.
19
- @timestamp_field = :created_at
20
26
  @serializer = PaperTrail::Serializers::YAML
21
- end
22
-
23
- def serialized_attributes
24
- ActiveSupport::Deprecation.warn(
25
- "PaperTrail.config.serialized_attributes is deprecated without " +
26
- "replacement and always returns false."
27
- )
28
- false
29
- end
30
-
31
- def serialized_attributes=(_)
32
- ActiveSupport::Deprecation.warn(
33
- "PaperTrail.config.serialized_attributes= is deprecated without " +
34
- "replacement and no longer has any effect."
35
- )
36
- end
37
-
38
- # Set the field which records when a version was created.
39
- # @api public
40
- # @deprecated
41
- def timestamp_field=(field_name)
42
- ::ActiveSupport::Deprecation.warn(
43
- "PaperTrail.config.timestamp_field= is deprecated without replacement." \
44
- "See https://github.com/airblade/paper_trail/pull/861 for discussion",
45
- caller(1)
46
- )
47
- @timestamp_field = field_name
48
- end
49
-
50
- # Previously, we checked `PaperTrail::VersionAssociation.table_exists?`
51
- # here, but that proved to be problematic in situations when the database
52
- # connection had not been established, or when the database does not exist
53
- # yet (as with `rake db:create`).
54
- def track_associations?
55
- if @track_associations.nil?
56
- ActiveSupport::Deprecation.warn <<-EOS.strip_heredoc.gsub(/\s+/, " ")
57
- PaperTrail.track_associations has not been set. As of PaperTrail 5, it
58
- defaults to false. Tracking associations is an experimental feature so
59
- we recommend setting PaperTrail.config.track_associations = false in
60
- your config/initializers/paper_trail.rb
61
- EOS
62
- false
63
- else
64
- @track_associations
65
- end
27
+ @has_paper_trail_defaults = {}
66
28
  end
67
29
 
68
30
  # Indicates whether PaperTrail is on or off. Default: true.
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Events
5
+ # We refer to times in the lifecycle of a record as "events". There are
6
+ # three events:
7
+ #
8
+ # - create
9
+ # - `after_create` we call `RecordTrail#record_create`
10
+ # - update
11
+ # - `after_update` we call `RecordTrail#record_update`
12
+ # - `after_touch` we call `RecordTrail#record_update`
13
+ # - `RecordTrail#save_with_version` calls `RecordTrail#record_update`
14
+ # - `RecordTrail#update_columns` is also referred to as an update, though
15
+ # it uses `RecordTrail#record_update_columns` rather than
16
+ # `RecordTrail#record_update`
17
+ # - destroy
18
+ # - `before_destroy` or `after_destroy` we call `RecordTrail#record_destroy`
19
+ #
20
+ # The value inserted into the `event` column of the versions table can also
21
+ # be overridden by the user, with `paper_trail_event`.
22
+ #
23
+ # @api private
24
+ class Base
25
+ RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
26
+
27
+ # @api private
28
+ def initialize(record, in_after_callback)
29
+ @record = record
30
+ @in_after_callback = in_after_callback
31
+ end
32
+
33
+ # Determines whether it is appropriate to generate a new version
34
+ # instance. A timestamp-only update (e.g. only `updated_at` changed) is
35
+ # considered notable unless an ignored attribute was also changed.
36
+ #
37
+ # @api private
38
+ def changed_notably?
39
+ if ignored_attr_has_changed?
40
+ timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
41
+ (notably_changed - timestamps).any?
42
+ else
43
+ notably_changed.any?
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
50
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
51
+ #
52
+ # @api private
53
+ def attribute_changed_in_latest_version?(attr_name)
54
+ if @in_after_callback && RAILS_GTE_5_1
55
+ @record.saved_change_to_attribute?(attr_name.to_s)
56
+ else
57
+ @record.attribute_changed?(attr_name.to_s)
58
+ end
59
+ end
60
+
61
+ # @api private
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
70
+ 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
85
+ end
86
+
87
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
88
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
89
+ #
90
+ # Event can be any of the three (create, update, destroy).
91
+ #
92
+ # @api private
93
+ def attribute_in_previous_version(attr_name, is_touch)
94
+ if RAILS_GTE_5_1
95
+ if @in_after_callback && !is_touch
96
+ # For most events, we want the original value of the attribute, before
97
+ # the last save.
98
+ @record.attribute_before_last_save(attr_name.to_s)
99
+ else
100
+ # We are either performing a `record_destroy` or a
101
+ # `record_update(is_touch: true)`.
102
+ @record.attribute_in_database(attr_name.to_s)
103
+ end
104
+ else
105
+ @record.attribute_was(attr_name.to_s)
106
+ end
107
+ end
108
+
109
+ # @api private
110
+ def calculated_ignored_array
111
+ ignore = @record.paper_trail_options[:ignore].dup
112
+ # Remove Hash arguments and then evaluate whether the attributes (the
113
+ # keys of the hash) should also get pushed into the collection.
114
+ ignore.delete_if do |obj|
115
+ obj.is_a?(Hash) &&
116
+ obj.each { |attr, condition|
117
+ ignore << attr if condition.respond_to?(:call) && condition.call(@record)
118
+ }
119
+ end
120
+ end
121
+
122
+ # @api private
123
+ def changed_and_not_ignored
124
+ skip = @record.paper_trail_options[:skip]
125
+ (changed_in_latest_version - calculated_ignored_array) - skip
126
+ end
127
+
128
+ # @api private
129
+ def changed_in_latest_version
130
+ # Memoized to reduce memory usage
131
+ @changed_in_latest_version ||= changes_in_latest_version.keys
132
+ end
133
+
134
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
135
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
136
+ #
137
+ # @api private
138
+ def changes_in_latest_version
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
146
+ end
147
+ end
148
+
149
+ # An attributed is "ignored" if it is listed in the `:ignore` option
150
+ # and/or the `:skip` option. Returns true if an ignored attribute has
151
+ # changed.
152
+ #
153
+ # @api private
154
+ def ignored_attr_has_changed?
155
+ ignored = calculated_ignored_array + @record.paper_trail_options[:skip]
156
+ ignored.any? && (changed_in_latest_version & ignored).any?
157
+ end
158
+
159
+ # PT 10 has a new optional column, `item_subtype`
160
+ #
161
+ # @api private
162
+ def merge_item_subtype_into(data)
163
+ if @record.class.paper_trail.version_class.columns_hash.key?("item_subtype")
164
+ data.merge!(item_subtype: @record.class.name)
165
+ end
166
+ end
167
+
168
+ # Updates `data` from the model's `meta` option and from `controller_info`.
169
+ # Metadata is always recorded; that means all three events (create, update,
170
+ # destroy) and `update_columns`.
171
+ #
172
+ # @api private
173
+ def merge_metadata_into(data)
174
+ merge_metadata_from_model_into(data)
175
+ merge_metadata_from_controller_into(data)
176
+ end
177
+
178
+ # Updates `data` from `controller_info`.
179
+ #
180
+ # @api private
181
+ def merge_metadata_from_controller_into(data)
182
+ data.merge(PaperTrail.request.controller_info || {})
183
+ end
184
+
185
+ # Updates `data` from the model's `meta` option.
186
+ #
187
+ # @api private
188
+ def merge_metadata_from_model_into(data)
189
+ @record.paper_trail_options[:meta].each do |k, v|
190
+ data[k] = model_metadatum(v, data[:event])
191
+ end
192
+ end
193
+
194
+ # Given a `value` from the model's `meta` option, returns an object to be
195
+ # persisted. The `value` can be a simple scalar value, but it can also
196
+ # be a symbol that names a model method, or even a Proc.
197
+ #
198
+ # @api private
199
+ def model_metadatum(value, event)
200
+ if value.respond_to?(:call)
201
+ value.call(@record)
202
+ elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
203
+ # If it is an attribute that is changing in an existing object,
204
+ # be sure to grab the current version.
205
+ if event != "create" &&
206
+ @record.has_attribute?(value) &&
207
+ attribute_changed_in_latest_version?(value)
208
+ attribute_in_previous_version(value, false)
209
+ else
210
+ @record.send(value)
211
+ end
212
+ else
213
+ value
214
+ end
215
+ end
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
+
224
+ # @api private
225
+ def notably_changed
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)
238
+ end
239
+ end
240
+
241
+ # Returns hash of attributes (with appropriate attributes serialized),
242
+ # omitting attributes to be skipped.
243
+ #
244
+ # @api private
245
+ def object_attrs_for_paper_trail(is_touch)
246
+ attrs = nonskipped_attributes_before_change(is_touch)
247
+ AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
248
+ attrs
249
+ end
250
+
251
+ # @api private
252
+ def prepare_object_changes(changes)
253
+ changes = serialize_object_changes(changes)
254
+ recordable_object_changes(changes)
255
+ end
256
+
257
+ # Returns an object which can be assigned to the `object_changes`
258
+ # attribute of a nascent version record. If the `object_changes` column is
259
+ # a postgres `json` column, then a hash can be used in the assignment,
260
+ # otherwise the column is a `text` column, and we must perform the
261
+ # serialization here, using `PaperTrail.serializer`.
262
+ #
263
+ # @api private
264
+ # @param changes HashWithIndifferentAccess
265
+ def recordable_object_changes(changes)
266
+ if PaperTrail.config.object_changes_adapter&.respond_to?(:diff)
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)
272
+ end
273
+
274
+ if @record.class.paper_trail.version_class.object_changes_col_is_json?
275
+ changes
276
+ else
277
+ PaperTrail.serializer.dump(changes)
278
+ end
279
+ end
280
+
281
+ # Returns a boolean indicating whether to store serialized version diffs
282
+ # in the `object_changes` column of the version record.
283
+ #
284
+ # @api private
285
+ def record_object_changes?
286
+ @record.class.paper_trail.version_class.column_names.include?("object_changes")
287
+ end
288
+
289
+ # Returns a boolean indicating whether to store the original object during save.
290
+ #
291
+ # @api private
292
+ def record_object?
293
+ @record.class.paper_trail.version_class.column_names.include?("object")
294
+ end
295
+
296
+ # Returns an object which can be assigned to the `object` attribute of a
297
+ # nascent version record. If the `object` column is a postgres `json`
298
+ # column, then a hash can be used in the assignment, otherwise the column
299
+ # is a `text` column, and we must perform the serialization here, using
300
+ # `PaperTrail.serializer`.
301
+ #
302
+ # @api private
303
+ def recordable_object(is_touch)
304
+ if @record.class.paper_trail.version_class.object_col_is_json?
305
+ object_attrs_for_paper_trail(is_touch)
306
+ else
307
+ PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch))
308
+ end
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
321
+ end
322
+ end
323
+ end