paper_trail 6.0.1 → 6.0.2

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