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