paper_trail 6.0.1 → 6.0.2

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.
@@ -10,36 +10,7 @@ module PaperTrail
10
10
  def reify(version, options)
11
11
  options = apply_defaults_to(options, version)
12
12
  attrs = version.object_deserialized
13
-
14
- # Normally a polymorphic belongs_to relationship allows us to get the
15
- # object we belong to by calling, in this case, `item`. However this
16
- # returns nil if `item` has been destroyed, and we need to be able to
17
- # retrieve destroyed objects.
18
- #
19
- # In this situation we constantize the `item_type` to get hold of the
20
- # class...except when the stored object's attributes include a `type`
21
- # key. If this is the case, the object we belong to is using single
22
- # table inheritance and the `item_type` will be the base class, not the
23
- # actual subclass. If `type` is present but empty, the class is the base
24
- # class.
25
- if options[:dup] != true && version.item
26
- model = version.item
27
- if options[:unversioned_attributes] == :nil
28
- init_unversioned_attrs(attrs, model)
29
- end
30
- else
31
- klass = version_reification_class(version, attrs)
32
- # The `dup` option always returns a new object, otherwise we should
33
- # attempt to look for the item outside of default scope(s).
34
- find_cond = { klass.primary_key => version.item_id }
35
- if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil?
36
- model = klass.new
37
- elsif options[:unversioned_attributes] == :nil
38
- model = item_found
39
- init_unversioned_attrs(attrs, model)
40
- end
41
- end
42
-
13
+ model = init_model(attrs, options, version)
43
14
  reify_attributes(model, version, attrs)
44
15
  model.send "#{model.class.version_association_name}=", version
45
16
  reify_associations(model, options, version)
@@ -71,6 +42,43 @@ module PaperTrail
71
42
  end
72
43
  end
73
44
 
45
+ # Initialize a model object suitable for reifying `version` into. Does
46
+ # not perform reification, merely instantiates the appropriate model
47
+ # class and, if specified by `options[:unversioned_attributes]`, sets
48
+ # unversioned attributes to `nil`.
49
+ #
50
+ # Normally a polymorphic belongs_to relationship allows us to get the
51
+ # object we belong to by calling, in this case, `item`. However this
52
+ # returns nil if `item` has been destroyed, and we need to be able to
53
+ # retrieve destroyed objects.
54
+ #
55
+ # In this situation we constantize the `item_type` to get hold of the
56
+ # class...except when the stored object's attributes include a `type`
57
+ # key. If this is the case, the object we belong to is using single
58
+ # table inheritance (STI) and the `item_type` will be the base class,
59
+ # not the actual subclass. If `type` is present but empty, the class is
60
+ # the base class.
61
+ def init_model(attrs, options, version)
62
+ if options[:dup] != true && version.item
63
+ model = version.item
64
+ if options[:unversioned_attributes] == :nil
65
+ init_unversioned_attrs(attrs, model)
66
+ end
67
+ else
68
+ klass = version_reification_class(version, attrs)
69
+ # The `dup` option always returns a new object, otherwise we should
70
+ # attempt to look for the item outside of default scope(s).
71
+ find_cond = { klass.primary_key => version.item_id }
72
+ if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil?
73
+ model = klass.new
74
+ elsif options[:unversioned_attributes] == :nil
75
+ model = item_found
76
+ init_unversioned_attrs(attrs, model)
77
+ end
78
+ end
79
+ model
80
+ end
81
+
74
82
  # Examine the `source_reflection`, i.e. the "source" of `assoc` the
75
83
  # `ThroughReflection`. The source can be a `BelongsToReflection`
76
84
  # or a `HasManyReflection`.
@@ -109,23 +117,12 @@ module PaperTrail
109
117
  end
110
118
 
111
119
  # @api private
112
- def hmt_collection_through_belongs_to(through_collection, assoc, options, transaction_id)
113
- collection_keys = through_collection.map { |through_model|
120
+ def hmt_collection_through_belongs_to(through_collection, assoc, options, tx_id)
121
+ ids = through_collection.map { |through_model|
114
122
  through_model.send(assoc.source_reflection.foreign_key)
115
123
  }
116
- version_id_subquery = assoc.klass.paper_trail.version_class.
117
- select("MIN(id)").
118
- where("item_type = ?", assoc.class_name).
119
- where("item_id IN (?)", collection_keys).
120
- where(
121
- "created_at >= ? OR transaction_id = ?",
122
- options[:version_at],
123
- transaction_id
124
- ).
125
- group("item_id").
126
- to_sql
127
- versions = versions_by_id(assoc.klass, version_id_subquery)
128
- collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
124
+ versions = load_versions_for_hmt_association(assoc, ids, tx_id, options[:version_at])
125
+ collection = Array.new assoc.klass.where(assoc.klass.primary_key => ids)
129
126
  prepare_array_for_has_many(collection, options, versions)
130
127
  collection
131
128
  end
@@ -137,6 +134,35 @@ module PaperTrail
137
134
  (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
138
135
  end
139
136
 
137
+ # Given a `belongs_to` association and a `version`, return a record that
138
+ # can be assigned in order to reify that association.
139
+ # @api private
140
+ def load_record_for_bt_association(assoc, id, options, version)
141
+ if version.nil?
142
+ assoc.klass.where(assoc.klass.primary_key => id).first
143
+ else
144
+ version.reify(
145
+ options.merge(
146
+ has_many: false,
147
+ has_one: false,
148
+ belongs_to: false,
149
+ has_and_belongs_to_many: false
150
+ )
151
+ )
152
+ end
153
+ end
154
+
155
+ # Given a `belongs_to` association and an `id`, return a version record
156
+ # from the point in time identified by `transaction_id` or `version_at`.
157
+ # @api private
158
+ def load_version_for_bt_association(assoc, id, transaction_id, version_at)
159
+ assoc.klass.paper_trail.version_class.
160
+ where("item_type = ?", assoc.class_name).
161
+ where("item_id = ?", id).
162
+ where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
163
+ order("id").limit(1).first
164
+ end
165
+
140
166
  # Given a HABTM association `assoc` and an `id`, return a version record
141
167
  # from the point in time identified by `transaction_id` or `version_at`.
142
168
  # @api private
@@ -164,6 +190,41 @@ module PaperTrail
164
190
  first
165
191
  end
166
192
 
193
+ # Given a `has_many` association on `model`, return the version records
194
+ # from the point in time identified by `tx_id` or `version_at`.
195
+ # @api private
196
+ def load_versions_for_hm_association(assoc, model, version_table, tx_id, version_at)
197
+ version_id_subquery = ::PaperTrail::VersionAssociation.
198
+ joins(model.class.version_association_name).
199
+ select("MIN(version_id)").
200
+ where("foreign_key_name = ?", assoc.foreign_key).
201
+ where("foreign_key_id = ?", model.id).
202
+ where("#{version_table}.item_type = ?", assoc.class_name).
203
+ where("created_at >= ? OR transaction_id = ?", version_at, tx_id).
204
+ group("item_id").
205
+ to_sql
206
+ versions_by_id(model.class, version_id_subquery)
207
+ end
208
+
209
+ # Given a `has_many(through:)` association and an array of `ids`, return
210
+ # the version records from the point in time identified by `tx_id` or
211
+ # `version_at`.
212
+ # @api private
213
+ def load_versions_for_hmt_association(assoc, ids, tx_id, version_at)
214
+ version_id_subquery = assoc.klass.paper_trail.version_class.
215
+ select("MIN(id)").
216
+ where("item_type = ?", assoc.class_name).
217
+ where("item_id IN (?)", ids).
218
+ where(
219
+ "created_at >= ? OR transaction_id = ?",
220
+ version_at,
221
+ tx_id
222
+ ).
223
+ group("item_id").
224
+ to_sql
225
+ versions_by_id(assoc.klass, version_id_subquery)
226
+ end
227
+
167
228
  # Set all the attributes in this version on the model.
168
229
  def reify_attributes(model, version, attrs)
169
230
  enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
@@ -179,7 +240,7 @@ module PaperTrail
179
240
  model[k.to_sym] = v
180
241
  elsif model.respond_to?("#{k}=")
181
242
  model.send("#{k}=", v)
182
- else
243
+ elsif version.logger
183
244
  version.logger.warn(
184
245
  "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
185
246
  )
@@ -240,76 +301,78 @@ module PaperTrail
240
301
  nil
241
302
  end
242
303
 
304
+ # @api private
243
305
  def reify_associations(model, options, version)
244
- reify_has_ones version.transaction_id, model, options if options[:has_one]
245
-
246
- reify_belongs_tos version.transaction_id, model, options if options[:belongs_to]
247
-
248
- reify_has_manys version.transaction_id, model, options if options[:has_many]
249
-
306
+ if options[:has_one]
307
+ reify_has_one_associations(version.transaction_id, model, options)
308
+ end
309
+ if options[:belongs_to]
310
+ reify_belongs_to_associations(version.transaction_id, model, options)
311
+ end
312
+ if options[:has_many]
313
+ reify_has_manys(version.transaction_id, model, options)
314
+ end
250
315
  if options[:has_and_belongs_to_many]
251
- reify_has_and_belongs_to_many version.transaction_id, model, options
316
+ reify_habtm_associations version.transaction_id, model, options
317
+ end
318
+ end
319
+
320
+ # Reify a single `has_one` association of `model`.
321
+ # @api private
322
+ def reify_has_one_association(assoc, model, options, transaction_id)
323
+ version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
324
+ return unless version
325
+ if version.event == "create"
326
+ if options[:mark_for_destruction]
327
+ model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
328
+ else
329
+ model.paper_trail.appear_as_new_record do
330
+ model.send "#{assoc.name}=", nil
331
+ end
332
+ end
333
+ else
334
+ child = version.reify(
335
+ options.merge(
336
+ has_many: false,
337
+ has_one: false,
338
+ belongs_to: false,
339
+ has_and_belongs_to_many: false
340
+ )
341
+ )
342
+ model.paper_trail.appear_as_new_record do
343
+ without_persisting(child) do
344
+ model.send "#{assoc.name}=", child
345
+ end
346
+ end
252
347
  end
253
348
  end
254
349
 
255
350
  # Restore the `model`'s has_one associations as they were when this
256
351
  # version was superseded by the next (because that's what the user was
257
352
  # looking at when they made the change).
258
- def reify_has_ones(transaction_id, model, options = {})
353
+ # @api private
354
+ def reify_has_one_associations(transaction_id, model, options = {})
259
355
  associations = model.class.reflect_on_all_associations(:has_one)
260
356
  each_enabled_association(associations) do |assoc|
261
- version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
262
- next unless version
263
- if version.event == "create"
264
- if options[:mark_for_destruction]
265
- model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
266
- else
267
- model.paper_trail.appear_as_new_record do
268
- model.send "#{assoc.name}=", nil
269
- end
270
- end
271
- else
272
- child = version.reify(
273
- options.merge(
274
- has_many: false,
275
- has_one: false,
276
- belongs_to: false,
277
- has_and_belongs_to_many: false
278
- )
279
- )
280
- model.paper_trail.appear_as_new_record do
281
- without_persisting(child) do
282
- model.send "#{assoc.name}=", child
283
- end
284
- end
285
- end
357
+ reify_has_one_association(assoc, model, options, transaction_id)
286
358
  end
287
359
  end
288
360
 
289
- def reify_belongs_tos(transaction_id, model, options = {})
361
+ # Reify a single `belongs_to` association of `model`.
362
+ # @api private
363
+ def reify_belongs_to_association(assoc, model, options, transaction_id)
364
+ id = model.send(assoc.association_foreign_key)
365
+ version = load_version_for_bt_association(assoc, id, transaction_id, options[:version_at])
366
+ record = load_record_for_bt_association(assoc, id, options, version)
367
+ model.send("#{assoc.name}=".to_sym, record)
368
+ end
369
+
370
+ # Reify all `belongs_to` associations of `model`.
371
+ # @api private
372
+ def reify_belongs_to_associations(transaction_id, model, options = {})
290
373
  associations = model.class.reflect_on_all_associations(:belongs_to)
291
374
  each_enabled_association(associations) do |assoc|
292
- collection_key = model.send(assoc.association_foreign_key)
293
- version = assoc.klass.paper_trail.version_class.
294
- where("item_type = ?", assoc.class_name).
295
- where("item_id = ?", collection_key).
296
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
297
- order("id").limit(1).first
298
-
299
- collection = if version.nil?
300
- assoc.klass.where(assoc.klass.primary_key => collection_key).first
301
- else
302
- version.reify(
303
- options.merge(
304
- has_many: false,
305
- has_one: false,
306
- belongs_to: false,
307
- has_and_belongs_to_many: false
308
- )
309
- )
310
- end
311
-
312
- model.send("#{assoc.name}=".to_sym, collection)
375
+ reify_belongs_to_association(assoc, model, options, transaction_id)
313
376
  end
314
377
  end
315
378
 
@@ -320,85 +383,101 @@ module PaperTrail
320
383
  assoc_has_many_through, assoc_has_many_directly =
321
384
  model.class.reflect_on_all_associations(:has_many).
322
385
  partition { |assoc| assoc.options[:through] }
323
- reify_has_many_directly(transaction_id, assoc_has_many_directly, model, options)
324
- reify_has_many_through(transaction_id, assoc_has_many_through, model, options)
386
+ reify_has_many_associations(transaction_id, assoc_has_many_directly, model, options)
387
+ reify_has_many_through_associations(transaction_id, assoc_has_many_through, model, options)
325
388
  end
326
389
 
327
- # Restore the `model`'s has_many associations not associated through
328
- # another association.
329
- def reify_has_many_directly(transaction_id, associations, model, options = {})
390
+ # Reify a single, direct (not `through`) `has_many` association of `model`.
391
+ # @api private
392
+ def reify_has_many_association(assoc, model, options, transaction_id, version_table_name)
393
+ versions = load_versions_for_hm_association(
394
+ assoc,
395
+ model,
396
+ version_table_name,
397
+ transaction_id,
398
+ options[:version_at]
399
+ )
400
+ collection = Array.new model.send(assoc.name).reload # to avoid cache
401
+ prepare_array_for_has_many(collection, options, versions)
402
+ model.send(assoc.name).proxy_association.target = collection
403
+ end
404
+
405
+ # Reify all direct (not `through`) `has_many` associations of `model`.
406
+ # @api private
407
+ def reify_has_many_associations(transaction_id, associations, model, options = {})
330
408
  version_table_name = model.class.paper_trail.version_class.table_name
331
409
  each_enabled_association(associations) do |assoc|
332
- version_id_subquery = PaperTrail::VersionAssociation.
333
- joins(model.class.version_association_name).
334
- select("MIN(version_id)").
335
- where("foreign_key_name = ?", assoc.foreign_key).
336
- where("foreign_key_id = ?", model.id).
337
- where("#{version_table_name}.item_type = ?", assoc.class_name).
338
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
339
- group("item_id").
340
- to_sql
341
- versions = versions_by_id(model.class, version_id_subquery)
342
- collection = Array.new model.send(assoc.name).reload # to avoid cache
343
- prepare_array_for_has_many(collection, options, versions)
344
- model.send(assoc.name).proxy_association.target = collection
410
+ reify_has_many_association(assoc, model, options, transaction_id, version_table_name)
345
411
  end
346
412
  end
347
413
 
348
- # Restore the `model`'s has_many associations through another association.
349
- # This must be called after the direct has_manys have been reified
350
- # (reify_has_many_directly).
351
- def reify_has_many_through(transaction_id, associations, model, options = {})
414
+ # Reify a single HMT association of `model`.
415
+ # @api private
416
+ def reify_has_many_through_association(assoc, model, options, transaction_id)
417
+ # Load the collection of through-models. For example, if `model` is a
418
+ # Chapter, having many Paragraphs through Sections, then
419
+ # `through_collection` will contain Sections.
420
+ through_collection = model.send(assoc.options[:through])
421
+
422
+ # Now, given the collection of "through" models (e.g. sections), load
423
+ # the collection of "target" models (e.g. paragraphs)
424
+ collection = hmt_collection(through_collection, assoc, options, transaction_id)
425
+
426
+ # Finally, assign the `collection` of "target" models, e.g. to
427
+ # `model.paragraphs`.
428
+ model.send(assoc.name).proxy_association.target = collection
429
+ end
430
+
431
+ # Reify all HMT associations of `model`. This must be called after the
432
+ # direct (non-`through`) has_manys have been reified.
433
+ # @api private
434
+ def reify_has_many_through_associations(transaction_id, associations, model, options = {})
352
435
  each_enabled_association(associations) do |assoc|
353
- # Load the collection of through-models. For example, if `model` is a
354
- # Chapter, having many Paragraphs through Sections, then
355
- # `through_collection` will contain Sections.
356
- through_collection = model.send(assoc.options[:through])
357
-
358
- # Now, given the collection of "through" models (e.g. sections), load
359
- # the collection of "target" models (e.g. paragraphs)
360
- collection = hmt_collection(through_collection, assoc, options, transaction_id)
361
-
362
- # Finally, assign the `collection` of "target" models, e.g. to
363
- # `model.paragraphs`.
364
- model.send(assoc.name).proxy_association.target = collection
436
+ reify_has_many_through_association(assoc, model, options, transaction_id)
365
437
  end
366
438
  end
367
439
 
368
- def reify_has_and_belongs_to_many(transaction_id, model, options = {})
440
+ # Reify a single HABTM association of `model`.
441
+ # @api private
442
+ def reify_habtm_association(assoc, model, options, papertrail_enabled, transaction_id)
443
+ version_ids = PaperTrail::VersionAssociation.
444
+ where("foreign_key_name = ?", assoc.name).
445
+ where("version_id = ?", transaction_id).
446
+ pluck(:foreign_key_id)
447
+
448
+ model.send(assoc.name).proxy_association.target =
449
+ version_ids.map do |id|
450
+ if papertrail_enabled
451
+ version = load_version_for_habtm(
452
+ assoc,
453
+ id,
454
+ transaction_id,
455
+ options[:version_at]
456
+ )
457
+ if version
458
+ next version.reify(
459
+ options.merge(
460
+ has_many: false,
461
+ has_one: false,
462
+ belongs_to: false,
463
+ has_and_belongs_to_many: false
464
+ )
465
+ )
466
+ end
467
+ end
468
+ assoc.klass.where(assoc.klass.primary_key => id).first
469
+ end
470
+ end
471
+
472
+ # Reify all HABTM associations of `model`.
473
+ # @api private
474
+ def reify_habtm_associations(transaction_id, model, options = {})
369
475
  model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
370
476
  papertrail_enabled = assoc.klass.paper_trail.enabled?
371
477
  next unless
372
478
  model.class.paper_trail_save_join_tables.include?(assoc.name) ||
373
479
  papertrail_enabled
374
-
375
- version_ids = PaperTrail::VersionAssociation.
376
- where("foreign_key_name = ?", assoc.name).
377
- where("version_id = ?", transaction_id).
378
- pluck(:foreign_key_id)
379
-
380
- model.send(assoc.name).proxy_association.target =
381
- version_ids.map do |id|
382
- if papertrail_enabled
383
- version = load_version_for_habtm(
384
- assoc,
385
- id,
386
- transaction_id,
387
- options[:version_at]
388
- )
389
- if version
390
- next version.reify(
391
- options.merge(
392
- has_many: false,
393
- has_one: false,
394
- belongs_to: false,
395
- has_and_belongs_to_many: false
396
- )
397
- )
398
- end
399
- end
400
- assoc.klass.where(assoc.klass.primary_key => id).first
401
- end
480
+ reify_habtm_association(assoc, model, options, papertrail_enabled, transaction_id)
402
481
  end
403
482
  end
404
483