paper_trail 4.2.0 → 5.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 +4 -4
- data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +28 -9
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +100 -0
- data/.rubocop_todo.yml +14 -0
- data/.travis.yml +8 -9
- data/Appraisals +41 -0
- data/CHANGELOG.md +49 -9
- data/Gemfile +1 -1
- data/README.md +130 -109
- data/Rakefile +19 -19
- data/doc/bug_report_template.rb +20 -14
- data/gemfiles/ar3.gemfile +10 -53
- data/gemfiles/ar4.gemfile +7 -0
- data/gemfiles/ar5.gemfile +13 -0
- data/lib/generators/paper_trail/install_generator.rb +26 -18
- data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +4 -2
- data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
- data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
- data/lib/generators/paper_trail/templates/create_versions.rb +39 -5
- data/lib/paper_trail.rb +169 -146
- data/lib/paper_trail/attributes_serialization.rb +89 -17
- data/lib/paper_trail/cleaner.rb +15 -9
- data/lib/paper_trail/config.rb +28 -11
- data/lib/paper_trail/frameworks/active_record.rb +4 -0
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
- data/lib/paper_trail/frameworks/cucumber.rb +1 -0
- data/lib/paper_trail/frameworks/rails.rb +2 -7
- data/lib/paper_trail/frameworks/rails/controller.rb +29 -9
- data/lib/paper_trail/frameworks/rails/engine.rb +7 -1
- data/lib/paper_trail/frameworks/rspec.rb +5 -5
- data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
- data/lib/paper_trail/frameworks/sinatra.rb +6 -4
- data/lib/paper_trail/has_paper_trail.rb +199 -106
- data/lib/paper_trail/record_history.rb +1 -3
- data/lib/paper_trail/reifier.rb +297 -118
- data/lib/paper_trail/serializers/json.rb +3 -3
- data/lib/paper_trail/serializers/yaml.rb +27 -8
- data/lib/paper_trail/version_association_concern.rb +3 -1
- data/lib/paper_trail/version_concern.rb +75 -35
- data/lib/paper_trail/version_number.rb +6 -9
- data/paper_trail.gemspec +44 -51
- data/spec/generators/install_generator_spec.rb +24 -25
- data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
- data/spec/models/animal_spec.rb +12 -12
- data/spec/models/boolit_spec.rb +8 -8
- data/spec/models/callback_modifier_spec.rb +47 -47
- data/spec/models/car_spec.rb +13 -0
- data/spec/models/fluxor_spec.rb +3 -3
- data/spec/models/gadget_spec.rb +19 -19
- data/spec/models/joined_version_spec.rb +3 -3
- data/spec/models/json_version_spec.rb +23 -24
- data/spec/models/kitchen/banana_spec.rb +3 -3
- data/spec/models/not_on_update_spec.rb +7 -4
- data/spec/models/post_with_status_spec.rb +13 -3
- data/spec/models/skipper_spec.rb +10 -10
- data/spec/models/thing_spec.rb +4 -4
- data/spec/models/truck_spec.rb +5 -0
- data/spec/models/vehicle_spec.rb +5 -0
- data/spec/models/version_spec.rb +103 -59
- data/spec/models/widget_spec.rb +82 -52
- data/spec/modules/paper_trail_spec.rb +2 -2
- data/spec/modules/version_concern_spec.rb +11 -12
- data/spec/modules/version_number_spec.rb +2 -4
- data/spec/paper_trail/config_spec.rb +10 -29
- data/spec/paper_trail_spec.rb +16 -14
- data/spec/rails_helper.rb +10 -9
- data/spec/requests/articles_spec.rb +11 -7
- data/spec/spec_helper.rb +41 -22
- data/spec/support/alt_db_init.rb +8 -13
- data/test/custom_json_serializer.rb +3 -3
- data/test/dummy/Rakefile +2 -2
- data/test/dummy/app/controllers/application_controller.rb +21 -8
- data/test/dummy/app/controllers/articles_controller.rb +11 -8
- data/test/dummy/app/controllers/widgets_controller.rb +13 -12
- data/test/dummy/app/models/animal.rb +1 -1
- data/test/dummy/app/models/article.rb +19 -11
- data/test/dummy/app/models/authorship.rb +1 -1
- data/test/dummy/app/models/bar_habtm.rb +4 -0
- data/test/dummy/app/models/book.rb +4 -4
- data/test/dummy/app/models/boolit.rb +1 -1
- data/test/dummy/app/models/callback_modifier.rb +6 -6
- data/test/dummy/app/models/car.rb +3 -0
- data/test/dummy/app/models/chapter.rb +4 -4
- data/test/dummy/app/models/customer.rb +1 -1
- data/test/dummy/app/models/document.rb +2 -2
- data/test/dummy/app/models/editor.rb +1 -1
- data/test/dummy/app/models/foo_habtm.rb +4 -0
- data/test/dummy/app/models/fruit.rb +2 -2
- data/test/dummy/app/models/gadget.rb +1 -1
- data/test/dummy/app/models/kitchen/banana.rb +1 -1
- data/test/dummy/app/models/legacy_widget.rb +2 -2
- data/test/dummy/app/models/line_item.rb +1 -1
- data/test/dummy/app/models/not_on_update.rb +1 -1
- data/test/dummy/app/models/person.rb +6 -6
- data/test/dummy/app/models/post.rb +1 -1
- data/test/dummy/app/models/post_with_status.rb +1 -1
- data/test/dummy/app/models/quotation.rb +1 -1
- data/test/dummy/app/models/section.rb +1 -1
- data/test/dummy/app/models/skipper.rb +2 -2
- data/test/dummy/app/models/song.rb +13 -4
- data/test/dummy/app/models/thing.rb +2 -2
- data/test/dummy/app/models/translation.rb +2 -2
- data/test/dummy/app/models/truck.rb +4 -0
- data/test/dummy/app/models/vehicle.rb +4 -0
- data/test/dummy/app/models/whatchamajigger.rb +1 -1
- data/test/dummy/app/models/widget.rb +7 -6
- data/test/dummy/app/versions/joined_version.rb +4 -3
- data/test/dummy/app/versions/json_version.rb +1 -1
- data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
- data/test/dummy/app/versions/post_version.rb +2 -2
- data/test/dummy/config.ru +1 -1
- data/test/dummy/config/application.rb +20 -9
- data/test/dummy/config/boot.rb +5 -5
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/development.rb +4 -3
- data/test/dummy/config/environments/production.rb +3 -2
- data/test/dummy/config/environments/test.rb +15 -5
- data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
- data/test/dummy/config/initializers/paper_trail.rb +1 -2
- data/test/dummy/config/initializers/secret_token.rb +3 -1
- data/test/dummy/config/initializers/session_store.rb +1 -1
- data/test/dummy/config/routes.rb +2 -2
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -74
- data/test/dummy/db/schema.rb +29 -6
- data/test/dummy/script/rails +6 -4
- data/test/functional/controller_test.rb +34 -35
- data/test/functional/enabled_for_controller_test.rb +6 -7
- data/test/functional/modular_sinatra_test.rb +43 -38
- data/test/functional/sinatra_test.rb +49 -40
- data/test/functional/thread_safety_test.rb +4 -6
- data/test/paper_trail_test.rb +15 -14
- data/test/test_helper.rb +68 -44
- data/test/time_travel_helper.rb +1 -15
- data/test/unit/associations_test.rb +517 -251
- data/test/unit/cleaner_test.rb +66 -60
- data/test/unit/inheritance_column_test.rb +17 -17
- data/test/unit/model_test.rb +611 -504
- data/test/unit/protected_attrs_test.rb +16 -12
- data/test/unit/serializer_test.rb +44 -43
- data/test/unit/serializers/json_test.rb +17 -18
- data/test/unit/serializers/mixin_json_test.rb +15 -14
- data/test/unit/serializers/mixin_yaml_test.rb +20 -16
- data/test/unit/serializers/yaml_test.rb +12 -13
- data/test/unit/timestamp_test.rb +10 -12
- data/test/unit/version_test.rb +7 -7
- metadata +92 -40
@@ -1,9 +1,7 @@
|
|
1
1
|
module PaperTrail
|
2
|
-
|
3
2
|
# Represents the history of a single record.
|
4
3
|
# @api private
|
5
4
|
class RecordHistory
|
6
|
-
|
7
5
|
# @param versions - ActiveRecord::Relation - All versions of the record.
|
8
6
|
# @param version_class - Class - Usually PaperTrail::Version,
|
9
7
|
# but it could also be a custom version class.
|
@@ -16,7 +14,7 @@ module PaperTrail
|
|
16
14
|
# Returns ordinal position of `version` in `sequence`.
|
17
15
|
# @api private
|
18
16
|
def index(version)
|
19
|
-
sequence.index(version)
|
17
|
+
sequence.to_a.index(version)
|
20
18
|
end
|
21
19
|
|
22
20
|
private
|
data/lib/paper_trail/reifier.rb
CHANGED
@@ -1,26 +1,13 @@
|
|
1
1
|
module PaperTrail
|
2
|
-
|
3
2
|
# Given a version record and some options, builds a new model object.
|
4
3
|
# @api private
|
5
4
|
module Reifier
|
6
5
|
class << self
|
7
|
-
|
8
6
|
# See `VersionConcern#reify` for documentation.
|
9
7
|
# @api private
|
10
8
|
def reify(version, options)
|
11
|
-
options = options
|
12
|
-
|
13
|
-
options.reverse_merge!(
|
14
|
-
:version_at => version.created_at,
|
15
|
-
:mark_for_destruction => false,
|
16
|
-
:has_one => false,
|
17
|
-
:has_many => false,
|
18
|
-
:unversioned_attributes => :nil
|
19
|
-
)
|
20
|
-
|
21
|
-
attrs = version.class.object_col_is_json? ?
|
22
|
-
version.object :
|
23
|
-
PaperTrail.serializer.load(version.object)
|
9
|
+
options = apply_defaults_to(options, version)
|
10
|
+
attrs = version.object_deserialized
|
24
11
|
|
25
12
|
# Normally a polymorphic belongs_to relationship allows us to get the
|
26
13
|
# object we belong to by calling, in this case, `item`. However this
|
@@ -33,37 +20,160 @@ module PaperTrail
|
|
33
20
|
# table inheritance and the `item_type` will be the base class, not the
|
34
21
|
# actual subclass. If `type` is present but empty, the class is the base
|
35
22
|
# class.
|
36
|
-
|
37
23
|
if options[:dup] != true && version.item
|
38
24
|
model = version.item
|
39
|
-
# Look for attributes that exist in the model and not in this
|
40
|
-
# version. These attributes should be set to nil.
|
41
25
|
if options[:unversioned_attributes] == :nil
|
42
|
-
(model
|
26
|
+
init_unversioned_attrs(attrs, model)
|
43
27
|
end
|
44
28
|
else
|
45
|
-
|
46
|
-
class_name = attrs[inheritance_column_name].blank? ?
|
47
|
-
version.item_type :
|
48
|
-
attrs[inheritance_column_name]
|
49
|
-
klass = class_name.constantize
|
29
|
+
klass = version_reification_class(version, attrs)
|
50
30
|
# The `dup` option always returns a new object, otherwise we should
|
51
31
|
# attempt to look for the item outside of default scope(s).
|
52
|
-
if options[:dup] || (
|
32
|
+
if options[:dup] || (item_found = klass.unscoped.find_by_id(version.item_id)).nil?
|
53
33
|
model = klass.new
|
54
34
|
elsif options[:unversioned_attributes] == :nil
|
55
|
-
model =
|
56
|
-
|
57
|
-
# version. These attributes should be set to nil.
|
58
|
-
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
|
35
|
+
model = item_found
|
36
|
+
init_unversioned_attrs(attrs, model)
|
59
37
|
end
|
60
38
|
end
|
61
39
|
|
40
|
+
reify_attributes(model, version, attrs)
|
41
|
+
model.send "#{model.class.version_association_name}=", version
|
42
|
+
reify_associations(model, options, version)
|
43
|
+
model
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Given a hash of `options` for `.reify`, return a new hash with default
|
49
|
+
# values applied.
|
50
|
+
# @api private
|
51
|
+
def apply_defaults_to(options, version)
|
52
|
+
{
|
53
|
+
version_at: version.created_at,
|
54
|
+
mark_for_destruction: false,
|
55
|
+
has_one: false,
|
56
|
+
has_many: false,
|
57
|
+
belongs_to: false,
|
58
|
+
has_and_belongs_to_many: false,
|
59
|
+
unversioned_attributes: :nil
|
60
|
+
}.merge(options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @api private
|
64
|
+
def each_enabled_association(associations)
|
65
|
+
associations.each do |assoc|
|
66
|
+
next unless assoc.klass.paper_trail_enabled_for_model?
|
67
|
+
yield assoc
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Examine the `source_reflection`, i.e. the "source" of `assoc` the
|
72
|
+
# `ThroughReflection`. The source can be a `BelongsToReflection`
|
73
|
+
# or a `HasManyReflection`.
|
74
|
+
#
|
75
|
+
# If the association is a has_many association again, then call
|
76
|
+
# reify_has_manys for each record in `through_collection`.
|
77
|
+
#
|
78
|
+
# @api private
|
79
|
+
def hmt_collection(through_collection, assoc, options, transaction_id)
|
80
|
+
if !assoc.source_reflection.belongs_to? && through_collection.present?
|
81
|
+
hmt_collection_through_has_many(
|
82
|
+
through_collection, assoc, options, transaction_id
|
83
|
+
)
|
84
|
+
else
|
85
|
+
hmt_collection_through_belongs_to(
|
86
|
+
through_collection, assoc, options, transaction_id
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
def hmt_collection_through_has_many(through_collection, assoc, options, transaction_id)
|
93
|
+
through_collection.each do |through_model|
|
94
|
+
reify_has_manys(transaction_id, through_model, options)
|
95
|
+
end
|
96
|
+
|
97
|
+
# At this point, the "through" part of the association chain has
|
98
|
+
# been reified, but not the final, "target" part. To continue our
|
99
|
+
# example, `model.sections` (including `model.sections.paragraphs`)
|
100
|
+
# has been loaded. However, the final "target" part of the
|
101
|
+
# association, that is, `model.paragraphs`, has not been loaded. So,
|
102
|
+
# we do that now.
|
103
|
+
through_collection.flat_map { |through_model|
|
104
|
+
through_model.public_send(assoc.name.to_sym).to_a
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
def hmt_collection_through_belongs_to(through_collection, assoc, options, transaction_id)
|
110
|
+
collection_keys = through_collection.map { |through_model|
|
111
|
+
through_model.send(assoc.source_reflection.foreign_key)
|
112
|
+
}
|
113
|
+
version_id_subquery = assoc.klass.paper_trail_version_class.
|
114
|
+
select("MIN(id)").
|
115
|
+
where("item_type = ?", assoc.class_name).
|
116
|
+
where("item_id IN (?)", collection_keys).
|
117
|
+
where(
|
118
|
+
"created_at >= ? OR transaction_id = ?",
|
119
|
+
options[:version_at],
|
120
|
+
transaction_id
|
121
|
+
).
|
122
|
+
group("item_id").
|
123
|
+
to_sql
|
124
|
+
versions = versions_by_id(assoc.klass, version_id_subquery)
|
125
|
+
collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
|
126
|
+
prepare_array_for_has_many(collection, options, versions)
|
127
|
+
collection
|
128
|
+
end
|
129
|
+
|
130
|
+
# Look for attributes that exist in `model` and not in this version.
|
131
|
+
# These attributes should be set to nil. Modifies `attrs`.
|
132
|
+
# @api private
|
133
|
+
def init_unversioned_attrs(attrs, model)
|
134
|
+
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
|
135
|
+
end
|
136
|
+
|
137
|
+
# Given a HABTM association `assoc` and an `id`, return a version record
|
138
|
+
# from the point in time identified by `transaction_id` or `version_at`.
|
139
|
+
# @api private
|
140
|
+
def load_version_for_habtm(assoc, id, transaction_id, version_at)
|
141
|
+
assoc.klass.paper_trail_version_class.
|
142
|
+
where("item_type = ?", assoc.klass.name).
|
143
|
+
where("item_id = ?", id).
|
144
|
+
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
145
|
+
order("id").
|
146
|
+
limit(1).
|
147
|
+
first
|
148
|
+
end
|
149
|
+
|
150
|
+
# Given a has-one association `assoc` on `model`, return the version
|
151
|
+
# record from the point in time identified by `transaction_id` or `version_at`.
|
152
|
+
# @api private
|
153
|
+
def load_version_for_has_one(assoc, model, transaction_id, version_at)
|
154
|
+
version_table_name = model.class.paper_trail_version_class.table_name
|
155
|
+
model.class.paper_trail_version_class.joins(:version_associations).
|
156
|
+
where("version_associations.foreign_key_name = ?", assoc.foreign_key).
|
157
|
+
where("version_associations.foreign_key_id = ?", model.id).
|
158
|
+
where("#{version_table_name}.item_type = ?", assoc.class_name).
|
159
|
+
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
160
|
+
order("#{version_table_name}.id ASC").
|
161
|
+
first
|
162
|
+
end
|
163
|
+
|
164
|
+
# Set all the attributes in this version on the model.
|
165
|
+
def reify_attributes(model, version, attrs)
|
166
|
+
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
|
62
167
|
model.class.unserialize_attributes_for_paper_trail! attrs
|
63
168
|
|
64
|
-
# Set all the attributes in this version on the model.
|
65
169
|
attrs.each do |k, v|
|
66
|
-
|
170
|
+
# `unserialize_attributes_for_paper_trail!` will return the mapped enum value
|
171
|
+
# and in Rails < 5, the []= uses the integer type caster from the column
|
172
|
+
# definition (in general) and thus will turn a (usually) string to 0 instead
|
173
|
+
# of the correct value
|
174
|
+
is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
|
175
|
+
|
176
|
+
if model.has_attribute?(k) && !is_enum_without_type_caster
|
67
177
|
model[k.to_sym] = v
|
68
178
|
elsif model.respond_to?("#{k}=")
|
69
179
|
model.send("#{k}=", v)
|
@@ -73,22 +183,8 @@ module PaperTrail
|
|
73
183
|
)
|
74
184
|
end
|
75
185
|
end
|
76
|
-
|
77
|
-
model.send "#{model.class.version_association_name}=", version
|
78
|
-
|
79
|
-
unless options[:has_one] == false
|
80
|
-
reify_has_ones version.transaction_id, model, options
|
81
|
-
end
|
82
|
-
|
83
|
-
unless options[:has_many] == false
|
84
|
-
reify_has_manys version.transaction_id, model, options
|
85
|
-
end
|
86
|
-
|
87
|
-
model
|
88
186
|
end
|
89
187
|
|
90
|
-
private
|
91
|
-
|
92
188
|
# Replaces each record in `array` with its reified version, if present
|
93
189
|
# in `versions`.
|
94
190
|
#
|
@@ -107,10 +203,17 @@ module PaperTrail
|
|
107
203
|
array.map! do |record|
|
108
204
|
if (version = versions.delete(record.id)).nil?
|
109
205
|
record
|
110
|
-
elsif version.event ==
|
111
|
-
options[:mark_for_destruction] ? record.tap
|
206
|
+
elsif version.event == "create"
|
207
|
+
options[:mark_for_destruction] ? record.tap(&:mark_for_destruction) : nil
|
112
208
|
else
|
113
|
-
version.reify(
|
209
|
+
version.reify(
|
210
|
+
options.merge(
|
211
|
+
has_many: false,
|
212
|
+
has_one: false,
|
213
|
+
belongs_to: false,
|
214
|
+
has_and_belongs_to_many: false
|
215
|
+
)
|
216
|
+
)
|
114
217
|
end
|
115
218
|
end
|
116
219
|
|
@@ -119,7 +222,14 @@ module PaperTrail
|
|
119
222
|
# associations.
|
120
223
|
array.concat(
|
121
224
|
versions.values.map { |v|
|
122
|
-
v.reify(
|
225
|
+
v.reify(
|
226
|
+
options.merge(
|
227
|
+
has_many: false,
|
228
|
+
has_one: false,
|
229
|
+
belongs_to: false,
|
230
|
+
has_and_belongs_to_many: false
|
231
|
+
)
|
232
|
+
)
|
123
233
|
}
|
124
234
|
)
|
125
235
|
|
@@ -128,40 +238,79 @@ module PaperTrail
|
|
128
238
|
nil
|
129
239
|
end
|
130
240
|
|
241
|
+
def reify_associations(model, options, version)
|
242
|
+
reify_has_ones version.transaction_id, model, options if options[:has_one]
|
243
|
+
|
244
|
+
reify_belongs_tos version.transaction_id, model, options if options[:belongs_to]
|
245
|
+
|
246
|
+
reify_has_manys version.transaction_id, model, options if options[:has_many]
|
247
|
+
|
248
|
+
if options[:has_and_belongs_to_many]
|
249
|
+
reify_has_and_belongs_to_many version.transaction_id, model, options
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
131
253
|
# Restore the `model`'s has_one associations as they were when this
|
132
254
|
# version was superseded by the next (because that's what the user was
|
133
255
|
# looking at when they made the change).
|
134
256
|
def reify_has_ones(transaction_id, model, options = {})
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
257
|
+
associations = model.class.reflect_on_all_associations(:has_one)
|
258
|
+
each_enabled_association(associations) do |assoc|
|
259
|
+
version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
|
260
|
+
next unless version
|
261
|
+
if version.event == "create"
|
262
|
+
if options[:mark_for_destruction]
|
263
|
+
model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
|
264
|
+
else
|
265
|
+
model.appear_as_new_record do
|
266
|
+
model.send "#{assoc.name}=", nil
|
267
|
+
end
|
268
|
+
end
|
269
|
+
else
|
270
|
+
child = version.reify(
|
271
|
+
options.merge(
|
272
|
+
has_many: false,
|
273
|
+
has_one: false,
|
274
|
+
belongs_to: false,
|
275
|
+
has_and_belongs_to_many: false
|
276
|
+
)
|
277
|
+
)
|
278
|
+
model.appear_as_new_record do
|
279
|
+
without_persisting(child) do
|
280
|
+
model.send "#{assoc.name}=", child
|
159
281
|
end
|
160
282
|
end
|
161
283
|
end
|
162
284
|
end
|
163
285
|
end
|
164
286
|
|
287
|
+
def reify_belongs_tos(transaction_id, model, options = {})
|
288
|
+
associations = model.class.reflect_on_all_associations(:belongs_to)
|
289
|
+
each_enabled_association(associations) do |assoc|
|
290
|
+
collection_key = model.send(assoc.association_foreign_key)
|
291
|
+
version = assoc.klass.paper_trail_version_class.
|
292
|
+
where("item_type = ?", assoc.class_name).
|
293
|
+
where("item_id = ?", collection_key).
|
294
|
+
where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
|
295
|
+
order("id").limit(1).first
|
296
|
+
|
297
|
+
collection = if version.nil?
|
298
|
+
assoc.klass.where(assoc.klass.primary_key => collection_key).first
|
299
|
+
else
|
300
|
+
version.reify(
|
301
|
+
options.merge(
|
302
|
+
has_many: false,
|
303
|
+
has_one: false,
|
304
|
+
belongs_to: false,
|
305
|
+
has_and_belongs_to_many: false
|
306
|
+
)
|
307
|
+
)
|
308
|
+
end
|
309
|
+
|
310
|
+
model.send("#{assoc.name}=".to_sym, collection)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
165
314
|
# Restore the `model`'s has_many associations as they were at version_at
|
166
315
|
# timestamp We lookup the first child versions after version_at timestamp or
|
167
316
|
# in same transaction.
|
@@ -177,8 +326,7 @@ module PaperTrail
|
|
177
326
|
# another association.
|
178
327
|
def reify_has_many_directly(transaction_id, associations, model, options = {})
|
179
328
|
version_table_name = model.class.paper_trail_version_class.table_name
|
180
|
-
associations
|
181
|
-
next unless assoc.klass.paper_trail_enabled_for_model?
|
329
|
+
each_enabled_association(associations) do |assoc|
|
182
330
|
version_id_subquery = PaperTrail::VersionAssociation.
|
183
331
|
joins(model.class.version_association_name).
|
184
332
|
select("MIN(version_id)").
|
@@ -189,7 +337,7 @@ module PaperTrail
|
|
189
337
|
group("item_id").
|
190
338
|
to_sql
|
191
339
|
versions = versions_by_id(model.class, version_id_subquery)
|
192
|
-
collection = Array.new model.send(assoc.name
|
340
|
+
collection = Array.new model.send(assoc.name).reload # to avoid cache
|
193
341
|
prepare_array_for_has_many(collection, options, versions)
|
194
342
|
model.send(assoc.name).proxy_association.target = collection
|
195
343
|
end
|
@@ -199,57 +347,76 @@ module PaperTrail
|
|
199
347
|
# This must be called after the direct has_manys have been reified
|
200
348
|
# (reify_has_many_directly).
|
201
349
|
def reify_has_many_through(transaction_id, associations, model, options = {})
|
202
|
-
associations
|
203
|
-
next unless assoc.klass.paper_trail_enabled_for_model?
|
204
|
-
|
350
|
+
each_enabled_association(associations) do |assoc|
|
205
351
|
# Load the collection of through-models. For example, if `model` is a
|
206
352
|
# Chapter, having many Paragraphs through Sections, then
|
207
353
|
# `through_collection` will contain Sections.
|
208
354
|
through_collection = model.send(assoc.options[:through])
|
209
355
|
|
210
|
-
#
|
211
|
-
#
|
212
|
-
|
213
|
-
#
|
214
|
-
# If the association is a has_many association again, then call
|
215
|
-
# reify_has_manys for each record in `through_collection`.
|
216
|
-
if !assoc.source_reflection.belongs_to? && through_collection.present?
|
217
|
-
through_collection.each do |through_model|
|
218
|
-
reify_has_manys(transaction_id, through_model, options)
|
219
|
-
end
|
356
|
+
# Now, given the collection of "through" models (e.g. sections), load
|
357
|
+
# the collection of "target" models (e.g. paragraphs)
|
358
|
+
collection = hmt_collection(through_collection, assoc, options, transaction_id)
|
220
359
|
|
221
|
-
|
222
|
-
|
223
|
-
# example, `model.sections` (including `model.sections.paragraphs`)
|
224
|
-
# has been loaded. However, the final "target" part of the
|
225
|
-
# association, that is, `model.paragraphs`, has not been loaded. So,
|
226
|
-
# we do that now.
|
227
|
-
collection = through_collection.map { |through_model|
|
228
|
-
through_model.send(assoc.name.to_sym).to_a
|
229
|
-
}.flatten
|
230
|
-
else
|
231
|
-
collection_keys = through_collection.map { |through_model|
|
232
|
-
through_model.send(assoc.association_foreign_key)
|
233
|
-
}
|
234
|
-
|
235
|
-
version_id_subquery = assoc.klass.paper_trail_version_class.
|
236
|
-
select("MIN(id)").
|
237
|
-
where("item_type = ?", assoc.class_name).
|
238
|
-
where("item_id IN (?)", collection_keys).
|
239
|
-
where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
|
240
|
-
group("item_id").
|
241
|
-
to_sql
|
242
|
-
versions = versions_by_id(assoc.klass, version_id_subquery)
|
243
|
-
collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
|
244
|
-
prepare_array_for_has_many(collection, options, versions)
|
245
|
-
end
|
246
|
-
|
247
|
-
# To continue our example above, assign to `model.paragraphs` the
|
248
|
-
# `collection` (an array of `Paragraph`s).
|
360
|
+
# Finally, assign the `collection` of "target" models, e.g. to
|
361
|
+
# `model.paragraphs`.
|
249
362
|
model.send(assoc.name).proxy_association.target = collection
|
250
363
|
end
|
251
364
|
end
|
252
365
|
|
366
|
+
def reify_has_and_belongs_to_many(transaction_id, model, options = {})
|
367
|
+
model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
|
368
|
+
papertrail_enabled = assoc.klass.paper_trail_enabled_for_model?
|
369
|
+
next unless
|
370
|
+
model.class.paper_trail_save_join_tables.include?(assoc.name) ||
|
371
|
+
papertrail_enabled
|
372
|
+
|
373
|
+
version_ids = PaperTrail::VersionAssociation.
|
374
|
+
where("foreign_key_name = ?", assoc.name).
|
375
|
+
where("version_id = ?", transaction_id).
|
376
|
+
pluck(:foreign_key_id)
|
377
|
+
|
378
|
+
model.send(assoc.name).proxy_association.target =
|
379
|
+
version_ids.map do |id|
|
380
|
+
if papertrail_enabled
|
381
|
+
version = load_version_for_habtm(
|
382
|
+
assoc,
|
383
|
+
id,
|
384
|
+
transaction_id,
|
385
|
+
options[:version_at]
|
386
|
+
)
|
387
|
+
if version
|
388
|
+
next version.reify(
|
389
|
+
options.merge(
|
390
|
+
has_many: false,
|
391
|
+
has_one: false,
|
392
|
+
belongs_to: false,
|
393
|
+
has_and_belongs_to_many: false
|
394
|
+
)
|
395
|
+
)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
assoc.klass.where(assoc.klass.primary_key => id).first
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Given a `version`, return the class to reify. This method supports
|
404
|
+
# Single Table Inheritance (STI) with custom inheritance columns.
|
405
|
+
#
|
406
|
+
# For example, imagine a `version` whose `item_type` is "Animal". The
|
407
|
+
# `animals` table is an STI table (it has cats and dogs) and it has a
|
408
|
+
# custom inheritance column, `species`. If `attrs["species"]` is "Dog",
|
409
|
+
# this method returns the constant `Dog`. If `attrs["species"]` is blank,
|
410
|
+
# this method returns the constant `Animal`. You can see this particular
|
411
|
+
# example in action in `spec/models/animal_spec.rb`.
|
412
|
+
#
|
413
|
+
def version_reification_class(version, attrs)
|
414
|
+
inheritance_column_name = version.item_type.constantize.inheritance_column
|
415
|
+
inher_col_value = attrs[inheritance_column_name]
|
416
|
+
class_name = inher_col_value.blank? ? version.item_type : inher_col_value
|
417
|
+
class_name.constantize
|
418
|
+
end
|
419
|
+
|
253
420
|
# Given a SQL fragment that identifies the IDs of version records,
|
254
421
|
# returns a `Hash` mapping those IDs to `Version`s.
|
255
422
|
#
|
@@ -263,7 +430,19 @@ module PaperTrail
|
|
263
430
|
klass.
|
264
431
|
paper_trail_version_class.
|
265
432
|
where("id IN (#{version_id_subquery})").
|
266
|
-
inject({}) { |
|
433
|
+
inject({}) { |a, e| a.merge!(e.item_id => e) }
|
434
|
+
end
|
435
|
+
|
436
|
+
# Temporarily suppress #save so we can reassociate with the reified
|
437
|
+
# master of a has_one relationship. Since ActiveRecord 5 the related
|
438
|
+
# object is saved when it is assigned to the association. ActiveRecord
|
439
|
+
# 5 also happens to be the first version that provides #suppress.
|
440
|
+
def without_persisting(record)
|
441
|
+
if record.class.respond_to? :suppress
|
442
|
+
record.class.suppress { yield }
|
443
|
+
else
|
444
|
+
yield
|
445
|
+
end
|
267
446
|
end
|
268
447
|
end
|
269
448
|
end
|