mongo_trails 10.3.1
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 +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +1 -0
- data/.travis.yml +13 -0
- data/Appraisals +7 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +20 -0
- data/README.md +36 -0
- data/Rakefile +13 -0
- data/gemfiles/rails_5.gemfile +9 -0
- data/gemfiles/rails_5.gemfile.lock +63 -0
- data/gemfiles/rails_6.gemfile +9 -0
- data/gemfiles/rails_6.gemfile.lock +63 -0
- data/lib/mongo_trails.rb +154 -0
- data/lib/mongo_trails/attribute_serializers/README.md +10 -0
- data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +27 -0
- data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +51 -0
- data/lib/mongo_trails/attribute_serializers/object_attribute.rb +41 -0
- data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +44 -0
- data/lib/mongo_trails/cleaner.rb +60 -0
- data/lib/mongo_trails/compatibility.rb +51 -0
- data/lib/mongo_trails/config.rb +41 -0
- data/lib/mongo_trails/events/base.rb +323 -0
- data/lib/mongo_trails/events/create.rb +32 -0
- data/lib/mongo_trails/events/destroy.rb +42 -0
- data/lib/mongo_trails/events/update.rb +60 -0
- data/lib/mongo_trails/frameworks/cucumber.rb +33 -0
- data/lib/mongo_trails/frameworks/rails.rb +4 -0
- data/lib/mongo_trails/frameworks/rails/controller.rb +109 -0
- data/lib/mongo_trails/frameworks/rails/engine.rb +43 -0
- data/lib/mongo_trails/frameworks/rspec.rb +43 -0
- data/lib/mongo_trails/frameworks/rspec/helpers.rb +29 -0
- data/lib/mongo_trails/has_paper_trail.rb +86 -0
- data/lib/mongo_trails/model_config.rb +249 -0
- data/lib/mongo_trails/mongo_support/config.rb +9 -0
- data/lib/mongo_trails/mongo_support/version.rb +56 -0
- data/lib/mongo_trails/queries/versions/where_object.rb +65 -0
- data/lib/mongo_trails/queries/versions/where_object_changes.rb +75 -0
- data/lib/mongo_trails/record_history.rb +51 -0
- data/lib/mongo_trails/record_trail.rb +304 -0
- data/lib/mongo_trails/reifier.rb +130 -0
- data/lib/mongo_trails/request.rb +166 -0
- data/lib/mongo_trails/serializers/json.rb +46 -0
- data/lib/mongo_trails/serializers/yaml.rb +43 -0
- data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +48 -0
- data/lib/mongo_trails/version_concern.rb +336 -0
- data/lib/mongo_trails/version_number.rb +23 -0
- data/mongo_trails.gemspec +38 -0
- metadata +180 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
Attribute Serializers
|
2
|
+
=====================
|
3
|
+
|
4
|
+
"Serialization" here refers to the preparation of data for insertion into a
|
5
|
+
database, particularly the `object` and `object_changes` columns in the
|
6
|
+
`versions` table.
|
7
|
+
|
8
|
+
Likewise, "deserialization" refers to any processing of data after they
|
9
|
+
have been read from the database, for example preparing the result of
|
10
|
+
`VersionConcern#changeset`.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mongo_trails/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
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mongo_trails/attribute_serializers/attribute_serializer_factory"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
# :nodoc:
|
7
|
+
module AttributeSerializers
|
8
|
+
# The `CastAttributeSerializer` (de)serializes model attribute values. For
|
9
|
+
# example, the string "1.99" serializes into the integer `1` when assigned
|
10
|
+
# to an attribute of type `ActiveRecord::Type::Integer`.
|
11
|
+
#
|
12
|
+
# This implementation depends on the `type_for_attribute` method, which was
|
13
|
+
# introduced in rails 4.2. As of PT 8, we no longer support rails < 4.2.
|
14
|
+
class CastAttributeSerializer
|
15
|
+
def initialize(klass)
|
16
|
+
@klass = klass
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Returns a hash mapping attributes to hashes that map strings to
|
22
|
+
# integers. Example:
|
23
|
+
#
|
24
|
+
# ```
|
25
|
+
# { "status" => { "draft"=>0, "published"=>1, "archived"=>2 } }
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# ActiveRecord::Enum was added in AR 4.1
|
29
|
+
# http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
|
30
|
+
def defined_enums
|
31
|
+
@defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Uses AR 5's `serialize` and `deserialize`.
|
36
|
+
class CastAttributeSerializer
|
37
|
+
def serialize(attr, val)
|
38
|
+
AttributeSerializerFactory.for(@klass, attr).serialize(val)
|
39
|
+
end
|
40
|
+
|
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)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mongo_trails/attribute_serializers/cast_attribute_serializer"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
module AttributeSerializers
|
7
|
+
# Serialize or deserialize the `version.object` column.
|
8
|
+
class ObjectAttribute
|
9
|
+
def initialize(model_class)
|
10
|
+
@model_class = model_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def serialize(attributes)
|
14
|
+
alter(attributes, :serialize)
|
15
|
+
end
|
16
|
+
|
17
|
+
def deserialize(attributes)
|
18
|
+
alter(attributes, :deserialize)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Modifies `attributes` in place.
|
24
|
+
# TODO: Return a new hash instead.
|
25
|
+
def alter(attributes, serialization_method)
|
26
|
+
# Don't serialize before values before inserting into columns of type
|
27
|
+
# `JSON` on `PostgreSQL` databases.
|
28
|
+
return attributes if object_col_is_json?
|
29
|
+
|
30
|
+
serializer = CastAttributeSerializer.new(@model_class)
|
31
|
+
attributes.each do |key, value|
|
32
|
+
attributes[key] = serializer.send(serialization_method, key, value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def object_col_is_json?
|
37
|
+
@model_class.paper_trail.version_class.object_col_is_json?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mongo_trails/attribute_serializers/cast_attribute_serializer"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
module AttributeSerializers
|
7
|
+
# Serialize or deserialize the `version.object_changes` column.
|
8
|
+
class ObjectChangesAttribute
|
9
|
+
def initialize(item_class)
|
10
|
+
@item_class = item_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def serialize(changes)
|
14
|
+
alter(changes, :serialize)
|
15
|
+
end
|
16
|
+
|
17
|
+
def deserialize(changes)
|
18
|
+
alter(changes, :deserialize)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Modifies `changes` in place.
|
24
|
+
# TODO: Return a new hash instead.
|
25
|
+
def alter(changes, serialization_method)
|
26
|
+
# Don't serialize before values before inserting into columns of type
|
27
|
+
# `JSON` on `PostgreSQL` databases.
|
28
|
+
return changes if object_changes_col_is_json?
|
29
|
+
|
30
|
+
serializer = CastAttributeSerializer.new(@item_class)
|
31
|
+
changes.clone.each do |key, change|
|
32
|
+
# `change` is an Array with two elements, representing before and after.
|
33
|
+
changes[key] = Array(change).map do |value|
|
34
|
+
serializer.send(serialization_method, key, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def object_changes_col_is_json?
|
40
|
+
@item_class.paper_trail.version_class.object_changes_col_is_json?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrail
|
4
|
+
# Utilities for deleting version records.
|
5
|
+
module Cleaner
|
6
|
+
# Destroys all but the most recent version(s) for items on a given date
|
7
|
+
# (or on all dates). Useful for deleting drafts.
|
8
|
+
#
|
9
|
+
# Options:
|
10
|
+
#
|
11
|
+
# - :keeping - An `integer` indicating the number of versions to be kept for
|
12
|
+
# each item per date. Defaults to `1`. The most recent matching versions
|
13
|
+
# are kept.
|
14
|
+
# - :date - Should either be a `Date` object specifying which date to
|
15
|
+
# destroy versions for or `:all`, which will specify that all dates
|
16
|
+
# should be cleaned. Defaults to `:all`.
|
17
|
+
# - :item_id - The `id` for the item to be cleaned on, or `nil`, which
|
18
|
+
# causes all items to be cleaned. Defaults to `nil`.
|
19
|
+
#
|
20
|
+
def clean_versions!(options = {})
|
21
|
+
options = { keeping: 1, date: :all }.merge(options)
|
22
|
+
gather_versions(options[:item_id], options[:date]).each do |_item_id, item_versions|
|
23
|
+
group_versions_by_date(item_versions).each do |_date, date_versions|
|
24
|
+
# Remove the number of versions we wish to keep from the collection
|
25
|
+
# of versions prior to destruction.
|
26
|
+
date_versions.pop(options[:keeping])
|
27
|
+
date_versions.map(&:destroy)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Returns a hash of versions grouped by the `item_id` attribute formatted
|
35
|
+
# like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
|
36
|
+
# set, versions will be narrowed to those pointing at items with those ids
|
37
|
+
# that were created on specified date. Versions are returned in
|
38
|
+
# chronological order.
|
39
|
+
def gather_versions(item_id = nil, date = :all)
|
40
|
+
unless date == :all || date.respond_to?(:to_date)
|
41
|
+
raise ArgumentError, "Expected date to be a Timestamp or :all"
|
42
|
+
end
|
43
|
+
versions = item_id ? PaperTrail::Version.where(item_id: item_id) : PaperTrail::Version
|
44
|
+
versions = versions.order(PaperTrail::Version.timestamp_sort_order)
|
45
|
+
versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
|
46
|
+
|
47
|
+
# If `versions` has not been converted to an ActiveRecord::Relation yet,
|
48
|
+
# do so now.
|
49
|
+
versions = PaperTrail::Version.all if versions == PaperTrail::Version
|
50
|
+
versions.group_by(&:item_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Given an array of versions, returns a hash mapping dates to arrays of
|
54
|
+
# versions.
|
55
|
+
# @api private
|
56
|
+
def group_versions_by_date(versions)
|
57
|
+
versions.group_by { |v| v.created_at.to_date }
|
58
|
+
end
|
59
|
+
end
|
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/mongo_trails/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/mongo_trails/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 mongo_trails/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
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
require "mongo_trails/serializers/yaml"
|
5
|
+
|
6
|
+
module PaperTrail
|
7
|
+
# Global configuration affecting all threads. Some thread-specific
|
8
|
+
# configuration can be found in `paper_trail.rb`, others in `controller.rb`.
|
9
|
+
class Config
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
attr_accessor(
|
13
|
+
:association_reify_error_behaviour,
|
14
|
+
:object_changes_adapter,
|
15
|
+
:serializer,
|
16
|
+
:version_limit,
|
17
|
+
:has_paper_trail_defaults,
|
18
|
+
:mongo_config,
|
19
|
+
:mongo_prefix
|
20
|
+
)
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
# Variables which affect all threads, whose access is synchronized.
|
24
|
+
@mutex = Mutex.new
|
25
|
+
@enabled = true
|
26
|
+
|
27
|
+
# Variables which affect all threads, whose access is *not* synchronized.
|
28
|
+
@serializer = PaperTrail::Serializers::YAML
|
29
|
+
@has_paper_trail_defaults = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Indicates whether PaperTrail is on or off. Default: true.
|
33
|
+
def enabled
|
34
|
+
@mutex.synchronize { !!@enabled }
|
35
|
+
end
|
36
|
+
|
37
|
+
def enabled=(enable)
|
38
|
+
@mutex.synchronize { @enabled = enable }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -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/mongo_trails/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
|
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) { yield }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
88
|
+
# https://github.com/paper-trail-gem/mongo_trails/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/mongo_trails/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.fields.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.fields.keys.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.fields.keys.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
|