paper_trail-association_tracking 0.0.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/CHANGELOG.md +8 -0
- data/LICENSE +20 -0
- data/README.md +252 -0
- data/Rakefile +61 -0
- data/lib/generators/paper_trail_association_tracking/install_generator.rb +64 -0
- data/lib/generators/paper_trail_association_tracking/templates/add_transaction_id_column_to_versions.rb.erb +13 -0
- data/lib/generators/paper_trail_association_tracking/templates/create_version_associations.rb.erb +22 -0
- data/lib/paper_trail-association_tracking.rb +68 -0
- data/lib/paper_trail_association_tracking/config.rb +26 -0
- data/lib/paper_trail_association_tracking/frameworks/active_record.rb +5 -0
- data/lib/paper_trail_association_tracking/frameworks/active_record/models/paper_trail/version_association.rb +13 -0
- data/lib/paper_trail_association_tracking/frameworks/rails.rb +3 -0
- data/lib/paper_trail_association_tracking/frameworks/rails/engine.rb +10 -0
- data/lib/paper_trail_association_tracking/frameworks/rspec.rb +20 -0
- data/lib/paper_trail_association_tracking/model_config.rb +76 -0
- data/lib/paper_trail_association_tracking/paper_trail.rb +38 -0
- data/lib/paper_trail_association_tracking/record_trail.rb +200 -0
- data/lib/paper_trail_association_tracking/reifier.rb +125 -0
- data/lib/paper_trail_association_tracking/reifiers/belongs_to.rb +50 -0
- data/lib/paper_trail_association_tracking/reifiers/has_and_belongs_to_many.rb +52 -0
- data/lib/paper_trail_association_tracking/reifiers/has_many.rb +112 -0
- data/lib/paper_trail_association_tracking/reifiers/has_many_through.rb +92 -0
- data/lib/paper_trail_association_tracking/reifiers/has_one.rb +135 -0
- data/lib/paper_trail_association_tracking/request.rb +32 -0
- data/lib/paper_trail_association_tracking/version.rb +5 -0
- data/lib/paper_trail_association_tracking/version_association_concern.rb +13 -0
- data/lib/paper_trail_association_tracking/version_concern.rb +37 -0
- metadata +260 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "paper_trail_association_tracking/reifiers/belongs_to"
|
4
|
+
require "paper_trail_association_tracking/reifiers/has_and_belongs_to_many"
|
5
|
+
require "paper_trail_association_tracking/reifiers/has_many"
|
6
|
+
require "paper_trail_association_tracking/reifiers/has_many_through"
|
7
|
+
require "paper_trail_association_tracking/reifiers/has_one"
|
8
|
+
|
9
|
+
module PaperTrailAssociationTracking
|
10
|
+
# Given a version record and some options, builds a new model object.
|
11
|
+
# @api private
|
12
|
+
module Reifier
|
13
|
+
module ClassMethods
|
14
|
+
# See `VersionConcern#reify` for documentation.
|
15
|
+
# @api private
|
16
|
+
def reify(version, options)
|
17
|
+
options = apply_defaults_to(options, version)
|
18
|
+
model = super
|
19
|
+
reify_associations(model, options, version)
|
20
|
+
model
|
21
|
+
end
|
22
|
+
|
23
|
+
# Restore the `model`'s has_many associations as they were at version_at
|
24
|
+
# timestamp We lookup the first child versions after version_at timestamp or
|
25
|
+
# in same transaction.
|
26
|
+
# @api private
|
27
|
+
def reify_has_manys(transaction_id, model, options = {})
|
28
|
+
assoc_has_many_through, assoc_has_many_directly =
|
29
|
+
model.class.reflect_on_all_associations(:has_many).
|
30
|
+
partition { |assoc| assoc.options[:through] }
|
31
|
+
reify_has_many_associations(transaction_id, assoc_has_many_directly, model, options)
|
32
|
+
reify_has_many_through_associations(transaction_id, assoc_has_many_through, model, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Given a hash of `options` for `.reify`, return a new hash with default
|
38
|
+
# values applied.
|
39
|
+
# @api private
|
40
|
+
def apply_defaults_to(options, version)
|
41
|
+
{
|
42
|
+
version_at: version.created_at,
|
43
|
+
mark_for_destruction: false,
|
44
|
+
has_one: false,
|
45
|
+
has_many: false,
|
46
|
+
belongs_to: false,
|
47
|
+
has_and_belongs_to_many: false,
|
48
|
+
unversioned_attributes: :nil
|
49
|
+
}.merge(options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def each_enabled_association(associations)
|
54
|
+
associations.each do |assoc|
|
55
|
+
next unless ::PaperTrail.request.enabled_for_model?(assoc.klass)
|
56
|
+
yield assoc
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
def reify_associations(model, options, version)
|
62
|
+
if options[:has_one]
|
63
|
+
reify_has_one_associations(version.transaction_id, model, options)
|
64
|
+
end
|
65
|
+
if options[:belongs_to]
|
66
|
+
reify_belongs_to_associations(version.transaction_id, model, options)
|
67
|
+
end
|
68
|
+
if options[:has_many]
|
69
|
+
reify_has_manys(version.transaction_id, model, options)
|
70
|
+
end
|
71
|
+
if options[:has_and_belongs_to_many]
|
72
|
+
reify_habtm_associations version.transaction_id, model, options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Restore the `model`'s has_one associations as they were when this
|
77
|
+
# version was superseded by the next (because that's what the user was
|
78
|
+
# looking at when they made the change).
|
79
|
+
# @api private
|
80
|
+
def reify_has_one_associations(transaction_id, model, options = {})
|
81
|
+
associations = model.class.reflect_on_all_associations(:has_one)
|
82
|
+
each_enabled_association(associations) do |assoc|
|
83
|
+
::PaperTrailAssociationTracking::Reifiers::HasOne.reify(assoc, model, options, transaction_id)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Reify all `belongs_to` associations of `model`.
|
88
|
+
# @api private
|
89
|
+
def reify_belongs_to_associations(transaction_id, model, options = {})
|
90
|
+
associations = model.class.reflect_on_all_associations(:belongs_to)
|
91
|
+
each_enabled_association(associations) do |assoc|
|
92
|
+
::PaperTrailAssociationTracking::Reifiers::BelongsTo.reify(assoc, model, options, transaction_id)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Reify all direct (not `through`) `has_many` associations of `model`.
|
97
|
+
# @api private
|
98
|
+
def reify_has_many_associations(transaction_id, associations, model, options = {})
|
99
|
+
version_table_name = model.class.paper_trail.version_class.table_name
|
100
|
+
each_enabled_association(associations) do |assoc|
|
101
|
+
::PaperTrailAssociationTracking::Reifiers::HasMany.reify(assoc, model, options, transaction_id, version_table_name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Reify all HMT associations of `model`. This must be called after the
|
106
|
+
# direct (non-`through`) has_manys have been reified.
|
107
|
+
# @api private
|
108
|
+
def reify_has_many_through_associations(transaction_id, associations, model, options = {})
|
109
|
+
each_enabled_association(associations) do |assoc|
|
110
|
+
::PaperTrailAssociationTracking::Reifiers::HasManyThrough.reify(assoc, model, options, transaction_id)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Reify all HABTM associations of `model`.
|
115
|
+
# @api private
|
116
|
+
def reify_habtm_associations(transaction_id, model, options = {})
|
117
|
+
model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
|
118
|
+
pt_enabled = ::PaperTrail.request.enabled_for_model?(assoc.klass)
|
119
|
+
next unless model.class.paper_trail_save_join_tables.include?(assoc.name) || pt_enabled
|
120
|
+
::PaperTrailAssociationTracking::Reifiers::HasAndBelongsToMany.reify(pt_enabled, assoc, model, options, transaction_id)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailAssociationTracking
|
4
|
+
module Reifiers
|
5
|
+
# Reify a single `belongs_to` association of `model`.
|
6
|
+
# @api private
|
7
|
+
module BelongsTo
|
8
|
+
class << self
|
9
|
+
# @api private
|
10
|
+
def reify(assoc, model, options, transaction_id)
|
11
|
+
id = model.send(assoc.foreign_key)
|
12
|
+
version = load_version(assoc, id, transaction_id, options[:version_at])
|
13
|
+
record = load_record(assoc, id, options, version)
|
14
|
+
model.send("#{assoc.name}=".to_sym, record)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Given a `belongs_to` association and a `version`, return a record that
|
20
|
+
# can be assigned in order to reify that association.
|
21
|
+
# @api private
|
22
|
+
def load_record(assoc, id, options, version)
|
23
|
+
if version.nil?
|
24
|
+
assoc.klass.where(assoc.klass.primary_key => id).first
|
25
|
+
else
|
26
|
+
version.reify(
|
27
|
+
options.merge(
|
28
|
+
has_many: false,
|
29
|
+
has_one: false,
|
30
|
+
belongs_to: false,
|
31
|
+
has_and_belongs_to_many: false
|
32
|
+
)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Given a `belongs_to` association and an `id`, return a version record
|
38
|
+
# from the point in time identified by `transaction_id` or `version_at`.
|
39
|
+
# @api private
|
40
|
+
def load_version(assoc, id, transaction_id, version_at)
|
41
|
+
assoc.klass.paper_trail.version_class.
|
42
|
+
where("item_type = ?", assoc.klass.base_class.name).
|
43
|
+
where("item_id = ?", id).
|
44
|
+
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
45
|
+
order("id").limit(1).first
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailAssociationTracking
|
4
|
+
module Reifiers
|
5
|
+
# Reify a single HABTM association of `model`.
|
6
|
+
# @api private
|
7
|
+
module HasAndBelongsToMany
|
8
|
+
class << self
|
9
|
+
# @api private
|
10
|
+
def reify(pt_enabled, assoc, model, options, transaction_id)
|
11
|
+
version_ids = ::PaperTrail::VersionAssociation.
|
12
|
+
where("foreign_key_name = ?", assoc.name).
|
13
|
+
where("version_id = ?", transaction_id).
|
14
|
+
pluck(:foreign_key_id)
|
15
|
+
|
16
|
+
model.send(assoc.name).proxy_association.target =
|
17
|
+
version_ids.map do |id|
|
18
|
+
if pt_enabled
|
19
|
+
version = load_version(assoc, id, transaction_id, options[:version_at])
|
20
|
+
if version
|
21
|
+
next version.reify(
|
22
|
+
options.merge(
|
23
|
+
has_many: false,
|
24
|
+
has_one: false,
|
25
|
+
belongs_to: false,
|
26
|
+
has_and_belongs_to_many: false
|
27
|
+
)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
assoc.klass.where(assoc.klass.primary_key => id).first
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Given a HABTM association `assoc` and an `id`, return a version record
|
38
|
+
# from the point in time identified by `transaction_id` or `version_at`.
|
39
|
+
# @api private
|
40
|
+
def load_version(assoc, id, transaction_id, version_at)
|
41
|
+
assoc.klass.paper_trail.version_class.
|
42
|
+
where("item_type = ?", assoc.klass.base_class.name).
|
43
|
+
where("item_id = ?", id).
|
44
|
+
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
45
|
+
order("id").
|
46
|
+
limit(1).
|
47
|
+
first
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailAssociationTracking
|
4
|
+
module Reifiers
|
5
|
+
# Reify a single, direct (not `through`) `has_many` association of `model`.
|
6
|
+
# @api private
|
7
|
+
module HasMany
|
8
|
+
class << self
|
9
|
+
# @api private
|
10
|
+
def reify(assoc, model, options, transaction_id, version_table_name)
|
11
|
+
versions = load_versions_for_hm_association(
|
12
|
+
assoc,
|
13
|
+
model,
|
14
|
+
version_table_name,
|
15
|
+
transaction_id,
|
16
|
+
options[:version_at]
|
17
|
+
)
|
18
|
+
collection = Array.new model.send(assoc.name).reload # to avoid cache
|
19
|
+
prepare_array(collection, options, versions)
|
20
|
+
model.send(assoc.name).proxy_association.target = collection
|
21
|
+
end
|
22
|
+
|
23
|
+
# Replaces each record in `array` with its reified version, if present
|
24
|
+
# in `versions`.
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
# @param array - The collection to be modified.
|
28
|
+
# @param options
|
29
|
+
# @param versions - A `Hash` mapping IDs to `Version`s
|
30
|
+
# @return nil - Always returns `nil`
|
31
|
+
#
|
32
|
+
# Once modified by this method, `array` will be assigned to the
|
33
|
+
# AR association currently being reified.
|
34
|
+
#
|
35
|
+
def prepare_array(array, options, versions)
|
36
|
+
# Iterate each child to replace it with the previous value if there is
|
37
|
+
# a version after the timestamp.
|
38
|
+
array.map! do |record|
|
39
|
+
if (version = versions.delete(record.id)).nil?
|
40
|
+
record
|
41
|
+
elsif version.event == "create"
|
42
|
+
options[:mark_for_destruction] ? record.tap(&:mark_for_destruction) : nil
|
43
|
+
else
|
44
|
+
version.reify(
|
45
|
+
options.merge(
|
46
|
+
has_many: false,
|
47
|
+
has_one: false,
|
48
|
+
belongs_to: false,
|
49
|
+
has_and_belongs_to_many: false
|
50
|
+
)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Reify the rest of the versions and add them to the collection, these
|
56
|
+
# versions are for those that have been removed from the live
|
57
|
+
# associations.
|
58
|
+
array.concat(
|
59
|
+
versions.values.map { |v|
|
60
|
+
v.reify(
|
61
|
+
options.merge(
|
62
|
+
has_many: false,
|
63
|
+
has_one: false,
|
64
|
+
belongs_to: false,
|
65
|
+
has_and_belongs_to_many: false
|
66
|
+
)
|
67
|
+
)
|
68
|
+
}
|
69
|
+
)
|
70
|
+
|
71
|
+
array.compact!
|
72
|
+
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Given a SQL fragment that identifies the IDs of version records,
|
77
|
+
# returns a `Hash` mapping those IDs to `Version`s.
|
78
|
+
#
|
79
|
+
# @api private
|
80
|
+
# @param klass - An ActiveRecord class.
|
81
|
+
# @param version_id_subquery - String. A SQL subquery that selects
|
82
|
+
# the IDs of version records.
|
83
|
+
# @return A `Hash` mapping IDs to `Version`s
|
84
|
+
#
|
85
|
+
def versions_by_id(klass, version_id_subquery)
|
86
|
+
klass.
|
87
|
+
paper_trail.version_class.
|
88
|
+
where("id IN (#{version_id_subquery})").
|
89
|
+
inject({}) { |a, e| a.merge!(e.item_id => e) }
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Given a `has_many` association on `model`, return the version records
|
95
|
+
# from the point in time identified by `tx_id` or `version_at`.
|
96
|
+
# @api private
|
97
|
+
def load_versions_for_hm_association(assoc, model, version_table, tx_id, version_at)
|
98
|
+
version_id_subquery = ::PaperTrail::VersionAssociation.
|
99
|
+
joins(model.class.version_association_name).
|
100
|
+
select("MIN(version_id)").
|
101
|
+
where("foreign_key_name = ?", assoc.foreign_key).
|
102
|
+
where("foreign_key_id = ?", model.id).
|
103
|
+
where("#{version_table}.item_type = ?", assoc.klass.base_class.name).
|
104
|
+
where("created_at >= ? OR transaction_id = ?", version_at, tx_id).
|
105
|
+
group("item_id").
|
106
|
+
to_sql
|
107
|
+
versions_by_id(model.class, version_id_subquery)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailAssociationTracking
|
4
|
+
module Reifiers
|
5
|
+
# Reify a single HMT association of `model`.
|
6
|
+
# @api private
|
7
|
+
module HasManyThrough
|
8
|
+
class << self
|
9
|
+
# @api private
|
10
|
+
def reify(assoc, model, options, transaction_id)
|
11
|
+
# Load the collection of through-models. For example, if `model` is a
|
12
|
+
# Chapter, having many Paragraphs through Sections, then
|
13
|
+
# `through_collection` will contain Sections.
|
14
|
+
through_collection = model.send(assoc.options[:through])
|
15
|
+
|
16
|
+
# Now, given the collection of "through" models (e.g. sections), load
|
17
|
+
# the collection of "target" models (e.g. paragraphs)
|
18
|
+
collection = collection(through_collection, assoc, options, transaction_id)
|
19
|
+
|
20
|
+
# Finally, assign the `collection` of "target" models, e.g. to
|
21
|
+
# `model.paragraphs`.
|
22
|
+
model.send(assoc.name).proxy_association.target = collection
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Examine the `source_reflection`, i.e. the "source" of `assoc` the
|
28
|
+
# `ThroughReflection`. The source can be a `BelongsToReflection`
|
29
|
+
# or a `HasManyReflection`.
|
30
|
+
#
|
31
|
+
# If the association is a has_many association again, then call
|
32
|
+
# reify_has_manys for each record in `through_collection`.
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
def collection(through_collection, assoc, options, transaction_id)
|
36
|
+
if !assoc.source_reflection.belongs_to? && through_collection.present?
|
37
|
+
collection_through_has_many(through_collection, assoc, options, transaction_id)
|
38
|
+
else
|
39
|
+
collection_through_belongs_to(through_collection, assoc, options, transaction_id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def collection_through_has_many(through_collection, assoc, options, transaction_id)
|
45
|
+
through_collection.each do |through_model|
|
46
|
+
::PaperTrail::Reifier.reify_has_manys(transaction_id, through_model, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
# At this point, the "through" part of the association chain has
|
50
|
+
# been reified, but not the final, "target" part. To continue our
|
51
|
+
# example, `model.sections` (including `model.sections.paragraphs`)
|
52
|
+
# has been loaded. However, the final "target" part of the
|
53
|
+
# association, that is, `model.paragraphs`, has not been loaded. So,
|
54
|
+
# we do that now.
|
55
|
+
through_collection.flat_map { |through_model|
|
56
|
+
through_model.public_send(assoc.name.to_sym).to_a
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
def collection_through_belongs_to(through_collection, assoc, options, tx_id)
|
62
|
+
ids = through_collection.map { |through_model|
|
63
|
+
through_model.send(assoc.source_reflection.foreign_key)
|
64
|
+
}
|
65
|
+
versions = load_versions_for_hmt_association(assoc, ids, tx_id, options[:version_at])
|
66
|
+
collection = Array.new assoc.klass.where(assoc.klass.primary_key => ids)
|
67
|
+
::PaperTrailAssociationTracking::Reifiers::HasMany.prepare_array(collection, options, versions)
|
68
|
+
collection
|
69
|
+
end
|
70
|
+
|
71
|
+
# Given a `has_many(through:)` association and an array of `ids`, return
|
72
|
+
# the version records from the point in time identified by `tx_id` or
|
73
|
+
# `version_at`.
|
74
|
+
# @api private
|
75
|
+
def load_versions_for_hmt_association(assoc, ids, tx_id, version_at)
|
76
|
+
version_id_subquery = assoc.klass.paper_trail.version_class.
|
77
|
+
select("MIN(id)").
|
78
|
+
where("item_type = ?", assoc.klass.base_class.name).
|
79
|
+
where("item_id IN (?)", ids).
|
80
|
+
where(
|
81
|
+
"created_at >= ? OR transaction_id = ?",
|
82
|
+
version_at,
|
83
|
+
tx_id
|
84
|
+
).
|
85
|
+
group("item_id").
|
86
|
+
to_sql
|
87
|
+
::PaperTrailAssociationTracking::Reifiers::HasMany.versions_by_id(assoc.klass, version_id_subquery)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaperTrailAssociationTracking
|
4
|
+
module Reifiers
|
5
|
+
# Reify a single `has_one` association of `model`.
|
6
|
+
# @api private
|
7
|
+
module HasOne
|
8
|
+
# A more helpful error message, instead of the AssociationTypeMismatch
|
9
|
+
# you would get if, eg. we were to try to assign a Bicycle to the :car
|
10
|
+
# association (before, if there were multiple records we would just take
|
11
|
+
# the first and hope for the best).
|
12
|
+
# @api private
|
13
|
+
class FoundMoreThanOne < RuntimeError
|
14
|
+
MESSAGE_FMT = <<~STR
|
15
|
+
Unable to reify has_one association. Expected to find one %s,
|
16
|
+
but found %d.
|
17
|
+
|
18
|
+
This is a known issue, and a good example of why association tracking
|
19
|
+
is an experimental feature that should not be used in production.
|
20
|
+
|
21
|
+
That said, this is a rare error. In spec/models/person_spec.rb we
|
22
|
+
reproduce it by having two STI models with the same foreign_key (Car
|
23
|
+
and Bicycle are both Vehicles and the FK for both is owner_id)
|
24
|
+
|
25
|
+
If you'd like to help fix this error, please read
|
26
|
+
https://github.com/airblade/paper_trail/issues/594
|
27
|
+
and see spec/models/person_spec.rb
|
28
|
+
STR
|
29
|
+
|
30
|
+
def initialize(base_class_name, num_records_found)
|
31
|
+
@base_class_name = base_class_name.to_s
|
32
|
+
@num_records_found = num_records_found.to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
def message
|
36
|
+
format(MESSAGE_FMT, @base_class_name, @num_records_found)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
# @api private
|
42
|
+
def reify(assoc, model, options, transaction_id)
|
43
|
+
version = load_version(assoc, model, transaction_id, options[:version_at])
|
44
|
+
return unless version
|
45
|
+
if version.event == "create"
|
46
|
+
create_event(assoc, model, options)
|
47
|
+
else
|
48
|
+
noncreate_event(assoc, model, options, version)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def create_event(assoc, model, options)
|
56
|
+
if options[:mark_for_destruction]
|
57
|
+
model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
|
58
|
+
else
|
59
|
+
model.paper_trail.appear_as_new_record do
|
60
|
+
model.send "#{assoc.name}=", nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Given a has-one association `assoc` on `model`, return the version
|
66
|
+
# record from the point in time identified by `transaction_id` or `version_at`.
|
67
|
+
# @api private
|
68
|
+
def load_version(assoc, model, transaction_id, version_at)
|
69
|
+
base_class_name = assoc.klass.base_class.name
|
70
|
+
versions = load_versions(assoc, model, transaction_id, version_at, base_class_name)
|
71
|
+
case versions.length
|
72
|
+
when 0
|
73
|
+
nil
|
74
|
+
when 1
|
75
|
+
versions.first
|
76
|
+
else
|
77
|
+
case ::PaperTrail.config.association_reify_error_behaviour
|
78
|
+
when "warn"
|
79
|
+
version = versions.first
|
80
|
+
version.logger&.warn(
|
81
|
+
FoundMoreThanOne.new(base_class_name, versions.length).message
|
82
|
+
)
|
83
|
+
version
|
84
|
+
when "ignore"
|
85
|
+
versions.first
|
86
|
+
else # "error"
|
87
|
+
raise FoundMoreThanOne.new(base_class_name, versions.length)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
def load_versions(assoc, model, transaction_id, version_at, base_class_name)
|
94
|
+
version_table_name = model.class.paper_trail.version_class.table_name
|
95
|
+
model.class.paper_trail.version_class.joins(:version_associations).
|
96
|
+
where("version_associations.foreign_key_name = ?", assoc.foreign_key).
|
97
|
+
where("version_associations.foreign_key_id = ?", model.id).
|
98
|
+
where("#{version_table_name}.item_type = ?", base_class_name).
|
99
|
+
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
100
|
+
order("#{version_table_name}.id ASC").
|
101
|
+
load
|
102
|
+
end
|
103
|
+
|
104
|
+
# @api private
|
105
|
+
def noncreate_event(assoc, model, options, version)
|
106
|
+
child = version.reify(
|
107
|
+
options.merge(
|
108
|
+
has_many: false,
|
109
|
+
has_one: false,
|
110
|
+
belongs_to: false,
|
111
|
+
has_and_belongs_to_many: false
|
112
|
+
)
|
113
|
+
)
|
114
|
+
model.paper_trail.appear_as_new_record do
|
115
|
+
without_persisting(child) do
|
116
|
+
model.send "#{assoc.name}=", child
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Temporarily suppress #save so we can reassociate with the reified
|
122
|
+
# master of a has_one relationship. Since ActiveRecord 5 the related
|
123
|
+
# object is saved when it is assigned to the association. ActiveRecord
|
124
|
+
# 5 also happens to be the first version that provides #suppress.
|
125
|
+
def without_persisting(record)
|
126
|
+
if record.class.respond_to? :suppress
|
127
|
+
record.class.suppress { yield }
|
128
|
+
else
|
129
|
+
yield
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|