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.
- checksums.yaml +5 -5
- data/lib/generators/paper_trail/install/USAGE +3 -0
- data/lib/generators/paper_trail/install/install_generator.rb +75 -0
- data/lib/generators/paper_trail/{templates/add_object_changes_to_versions.rb → install/templates/add_object_changes_to_versions.rb.erb} +1 -1
- data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
- data/lib/generators/paper_trail/migration_generator.rb +38 -0
- data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
- data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
- data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
- data/lib/paper_trail.rb +82 -130
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +15 -44
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
- data/lib/paper_trail/cleaner.rb +3 -1
- data/lib/paper_trail/compatibility.rb +51 -0
- data/lib/paper_trail/config.rb +11 -49
- data/lib/paper_trail/events/base.rb +323 -0
- data/lib/paper_trail/events/create.rb +32 -0
- data/lib/paper_trail/events/destroy.rb +42 -0
- data/lib/paper_trail/events/update.rb +60 -0
- data/lib/paper_trail/frameworks/active_record.rb +2 -1
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +8 -3
- data/lib/paper_trail/frameworks/cucumber.rb +5 -3
- data/lib/paper_trail/frameworks/rails.rb +2 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +33 -43
- data/lib/paper_trail/frameworks/rails/engine.rb +34 -1
- data/lib/paper_trail/frameworks/rspec.rb +17 -4
- data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
- data/lib/paper_trail/has_paper_trail.rb +22 -310
- data/lib/paper_trail/model_config.rb +157 -109
- data/lib/paper_trail/queries/versions/where_object.rb +65 -0
- data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
- data/lib/paper_trail/record_history.rb +3 -9
- data/lib/paper_trail/record_trail.rb +169 -319
- data/lib/paper_trail/reifier.rb +53 -374
- data/lib/paper_trail/request.rb +166 -0
- data/lib/paper_trail/serializers/json.rb +9 -10
- data/lib/paper_trail/serializers/yaml.rb +15 -28
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
- data/lib/paper_trail/version_concern.rb +160 -155
- data/lib/paper_trail/version_number.rb +12 -4
- metadata +77 -372
- data/.github/CONTRIBUTING.md +0 -109
- data/.github/ISSUE_TEMPLATE.md +0 -13
- data/.gitignore +0 -23
- data/.rspec +0 -2
- data/.rubocop.yml +0 -99
- data/.rubocop_todo.yml +0 -22
- data/.travis.yml +0 -41
- data/Appraisals +0 -38
- data/CHANGELOG.md +0 -560
- data/Gemfile +0 -2
- data/MIT-LICENSE +0 -20
- data/README.md +0 -1654
- data/Rakefile +0 -30
- data/doc/bug_report_template.rb +0 -69
- data/doc/warning_about_not_setting_whodunnit.md +0 -32
- data/gemfiles/ar3.gemfile +0 -19
- data/gemfiles/ar4.gemfile +0 -8
- data/gemfiles/ar5.gemfile +0 -9
- data/lib/generators/paper_trail/USAGE +0 -2
- data/lib/generators/paper_trail/default_initializer.rb +0 -0
- data/lib/generators/paper_trail/install_generator.rb +0 -57
- data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -13
- data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -22
- data/lib/generators/paper_trail/templates/create_versions.rb +0 -80
- data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -11
- data/lib/paper_trail/frameworks/sinatra.rb +0 -40
- data/lib/paper_trail/version_association_concern.rb +0 -17
- data/paper_trail.gemspec +0 -56
- data/spec/generators/install_generator_spec.rb +0 -66
- data/spec/generators/paper_trail/templates/create_versions_spec.rb +0 -51
- data/spec/models/animal_spec.rb +0 -36
- data/spec/models/boolit_spec.rb +0 -48
- data/spec/models/callback_modifier_spec.rb +0 -96
- data/spec/models/car_spec.rb +0 -13
- data/spec/models/custom_primary_key_record_spec.rb +0 -18
- data/spec/models/fluxor_spec.rb +0 -17
- data/spec/models/gadget_spec.rb +0 -68
- data/spec/models/joined_version_spec.rb +0 -47
- data/spec/models/json_version_spec.rb +0 -102
- data/spec/models/kitchen/banana_spec.rb +0 -14
- data/spec/models/not_on_update_spec.rb +0 -22
- data/spec/models/post_with_status_spec.rb +0 -50
- data/spec/models/skipper_spec.rb +0 -46
- data/spec/models/thing_spec.rb +0 -11
- data/spec/models/truck_spec.rb +0 -5
- data/spec/models/vehicle_spec.rb +0 -5
- data/spec/models/version_spec.rb +0 -272
- data/spec/models/widget_spec.rb +0 -343
- data/spec/modules/paper_trail_spec.rb +0 -27
- data/spec/modules/version_concern_spec.rb +0 -31
- data/spec/modules/version_number_spec.rb +0 -43
- data/spec/paper_trail/config_spec.rb +0 -33
- data/spec/paper_trail_spec.rb +0 -79
- data/spec/rails_helper.rb +0 -34
- data/spec/requests/articles_spec.rb +0 -34
- data/spec/spec_helper.rb +0 -114
- data/spec/support/alt_db_init.rb +0 -54
- data/test/custom_json_serializer.rb +0 -13
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -33
- data/test/dummy/app/controllers/articles_controller.rb +0 -20
- data/test/dummy/app/controllers/test_controller.rb +0 -5
- data/test/dummy/app/controllers/widgets_controller.rb +0 -32
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/models/animal.rb +0 -6
- data/test/dummy/app/models/article.rb +0 -24
- data/test/dummy/app/models/authorship.rb +0 -5
- data/test/dummy/app/models/bar_habtm.rb +0 -4
- data/test/dummy/app/models/book.rb +0 -9
- data/test/dummy/app/models/boolit.rb +0 -4
- data/test/dummy/app/models/callback_modifier.rb +0 -45
- data/test/dummy/app/models/car.rb +0 -3
- data/test/dummy/app/models/cat.rb +0 -2
- data/test/dummy/app/models/chapter.rb +0 -9
- data/test/dummy/app/models/citation.rb +0 -5
- data/test/dummy/app/models/custom_primary_key_record.rb +0 -13
- data/test/dummy/app/models/customer.rb +0 -4
- data/test/dummy/app/models/document.rb +0 -4
- data/test/dummy/app/models/dog.rb +0 -2
- data/test/dummy/app/models/editor.rb +0 -4
- data/test/dummy/app/models/editorship.rb +0 -5
- data/test/dummy/app/models/elephant.rb +0 -3
- data/test/dummy/app/models/fluxor.rb +0 -3
- data/test/dummy/app/models/foo_habtm.rb +0 -5
- data/test/dummy/app/models/foo_widget.rb +0 -2
- data/test/dummy/app/models/fruit.rb +0 -5
- data/test/dummy/app/models/gadget.rb +0 -3
- data/test/dummy/app/models/kitchen/banana.rb +0 -5
- data/test/dummy/app/models/legacy_widget.rb +0 -4
- data/test/dummy/app/models/line_item.rb +0 -4
- data/test/dummy/app/models/not_on_update.rb +0 -4
- data/test/dummy/app/models/order.rb +0 -5
- data/test/dummy/app/models/paragraph.rb +0 -5
- data/test/dummy/app/models/person.rb +0 -38
- data/test/dummy/app/models/post.rb +0 -3
- data/test/dummy/app/models/post_with_status.rb +0 -8
- data/test/dummy/app/models/protected_widget.rb +0 -3
- data/test/dummy/app/models/quotation.rb +0 -5
- data/test/dummy/app/models/section.rb +0 -6
- data/test/dummy/app/models/skipper.rb +0 -6
- data/test/dummy/app/models/song.rb +0 -41
- data/test/dummy/app/models/thing.rb +0 -3
- data/test/dummy/app/models/translation.rb +0 -4
- data/test/dummy/app/models/truck.rb +0 -4
- data/test/dummy/app/models/vehicle.rb +0 -4
- data/test/dummy/app/models/whatchamajigger.rb +0 -4
- data/test/dummy/app/models/widget.rb +0 -16
- data/test/dummy/app/models/wotsit.rb +0 -8
- data/test/dummy/app/versions/custom_primary_key_record_version.rb +0 -3
- data/test/dummy/app/versions/joined_version.rb +0 -6
- data/test/dummy/app/versions/json_version.rb +0 -3
- data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
- data/test/dummy/app/versions/post_version.rb +0 -3
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -80
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.mysql.yml +0 -19
- data/test/dummy/config/database.postgres.yml +0 -15
- data/test/dummy/config/database.sqlite.yml +0 -15
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -74
- data/test/dummy/config/environments/test.rb +0 -51
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -9
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/paper_trail.rb +0 -9
- data/test/dummy/config/initializers/secret_token.rb +0 -9
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -4
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -361
- data/test/dummy/db/schema.rb +0 -288
- data/test/dummy/script/rails +0 -8
- data/test/functional/controller_test.rb +0 -90
- data/test/functional/enabled_for_controller_test.rb +0 -28
- data/test/functional/modular_sinatra_test.rb +0 -46
- data/test/functional/sinatra_test.rb +0 -51
- data/test/functional/thread_safety_test.rb +0 -46
- data/test/test_helper.rb +0 -127
- data/test/time_travel_helper.rb +0 -1
- data/test/unit/associations_test.rb +0 -1016
- data/test/unit/cleaner_test.rb +0 -188
- data/test/unit/inheritance_column_test.rb +0 -43
- data/test/unit/model_test.rb +0 -1489
- data/test/unit/protected_attrs_test.rb +0 -52
- data/test/unit/serializer_test.rb +0 -119
- data/test/unit/serializers/json_test.rb +0 -95
- data/test/unit/serializers/mixin_json_test.rb +0 -37
- data/test/unit/serializers/mixin_yaml_test.rb +0 -53
- data/test/unit/serializers/yaml_test.rb +0 -54
- data/test/unit/timestamp_test.rb +0 -41
- 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.
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
data/lib/paper_trail/cleaner.rb
CHANGED
@@ -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.
|
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
|
data/lib/paper_trail/config.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|