mongoid-history 0.8.3 → 0.8.5
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/.coveralls.yml +1 -1
- data/.document +5 -5
- data/.github/workflows/test.yml +72 -0
- data/.gitignore +46 -46
- data/.rspec +2 -2
- data/.rubocop.yml +6 -6
- data/.rubocop_todo.yml +99 -99
- data/CHANGELOG.md +173 -163
- data/CONTRIBUTING.md +117 -118
- data/Dangerfile +1 -1
- data/Gemfile +49 -40
- data/LICENSE.txt +20 -20
- data/README.md +609 -608
- data/RELEASING.md +66 -67
- data/Rakefile +24 -24
- data/UPGRADING.md +53 -53
- data/lib/mongoid/history/attributes/base.rb +72 -72
- data/lib/mongoid/history/attributes/create.rb +45 -45
- data/lib/mongoid/history/attributes/destroy.rb +34 -34
- data/lib/mongoid/history/attributes/update.rb +104 -104
- data/lib/mongoid/history/options.rb +177 -177
- data/lib/mongoid/history/trackable.rb +588 -583
- data/lib/mongoid/history/tracker.rb +247 -247
- data/lib/mongoid/history/version.rb +5 -5
- data/lib/mongoid/history.rb +77 -77
- data/lib/mongoid-history.rb +1 -1
- data/mongoid-history.gemspec +25 -25
- data/perf/benchmark_modified_attributes_for_create.rb +65 -65
- data/perf/gc_suite.rb +21 -21
- data/spec/integration/embedded_in_polymorphic_spec.rb +112 -112
- data/spec/integration/integration_spec.rb +976 -976
- data/spec/integration/multi_relation_spec.rb +47 -47
- data/spec/integration/multiple_trackers_spec.rb +68 -68
- data/spec/integration/nested_embedded_documents_spec.rb +64 -64
- data/spec/integration/nested_embedded_documents_tracked_in_parent_spec.rb +124 -124
- data/spec/integration/nested_embedded_polymorphic_documents_spec.rb +115 -115
- data/spec/integration/subclasses_spec.rb +47 -47
- data/spec/integration/track_history_order_spec.rb +84 -84
- data/spec/integration/validation_failure_spec.rb +76 -76
- data/spec/spec_helper.rb +32 -30
- data/spec/support/error_helpers.rb +7 -0
- data/spec/support/mongoid.rb +11 -11
- data/spec/support/mongoid_history.rb +12 -12
- data/spec/unit/attributes/base_spec.rb +141 -141
- data/spec/unit/attributes/create_spec.rb +342 -342
- data/spec/unit/attributes/destroy_spec.rb +228 -228
- data/spec/unit/attributes/update_spec.rb +342 -342
- data/spec/unit/callback_options_spec.rb +165 -165
- data/spec/unit/embedded_methods_spec.rb +87 -87
- data/spec/unit/history_spec.rb +58 -58
- data/spec/unit/my_instance_methods_spec.rb +555 -555
- data/spec/unit/options_spec.rb +365 -365
- data/spec/unit/singleton_methods_spec.rb +406 -406
- data/spec/unit/store/default_store_spec.rb +11 -11
- data/spec/unit/store/request_store_spec.rb +13 -13
- data/spec/unit/trackable_spec.rb +1057 -987
- data/spec/unit/tracker_spec.rb +190 -190
- metadata +9 -7
- data/.travis.yml +0 -36
@@ -1,583 +1,588 @@
|
|
1
|
-
module Mongoid
|
2
|
-
module History
|
3
|
-
module Trackable
|
4
|
-
extend ActiveSupport::Concern
|
5
|
-
|
6
|
-
module ClassMethods
|
7
|
-
def track_history(options = {})
|
8
|
-
extend RelationMethods
|
9
|
-
|
10
|
-
history_options = Mongoid::History::Options.new(self, options)
|
11
|
-
|
12
|
-
field history_options.options[:version_field].to_sym, type: Integer
|
13
|
-
|
14
|
-
unless history_options.options[:modifier_field].nil?
|
15
|
-
belongs_to_modifier_options = { class_name: Mongoid::History.modifier_class_name }
|
16
|
-
belongs_to_modifier_options[:inverse_of] = history_options.options[:modifier_field_inverse_of] if history_options.options.key?(:modifier_field_inverse_of)
|
17
|
-
belongs_to_modifier_options[:optional] = true if history_options.options[:modifier_field_optional] && Mongoid::Compatibility::Version.mongoid6_or_newer?
|
18
|
-
belongs_to history_options.options[:modifier_field].to_sym, belongs_to_modifier_options
|
19
|
-
end
|
20
|
-
|
21
|
-
include MyInstanceMethods
|
22
|
-
extend SingletonMethods
|
23
|
-
|
24
|
-
delegate :history_trackable_options, to: 'self.class'
|
25
|
-
delegate :track_history?, to: 'self.class'
|
26
|
-
|
27
|
-
callback_options = history_options.options.slice(:if, :unless)
|
28
|
-
around_update :track_update, **callback_options if history_options.options[:track_update]
|
29
|
-
around_create :track_create, **callback_options if history_options.options[:track_create]
|
30
|
-
around_destroy :track_destroy, **callback_options if history_options.options[:track_destroy]
|
31
|
-
|
32
|
-
unless respond_to? :mongoid_history_options
|
33
|
-
class_attribute :mongoid_history_options, instance_accessor: false
|
34
|
-
end
|
35
|
-
|
36
|
-
self.mongoid_history_options = history_options
|
37
|
-
end
|
38
|
-
|
39
|
-
def history_settings(options = {})
|
40
|
-
options = Mongoid::History.default_settings.merge(options.symbolize_keys)
|
41
|
-
options = options.slice(*Mongoid::History.default_settings.keys)
|
42
|
-
options[:paranoia_field] = aliased_fields[options[:paranoia_field].to_s] || options[:paranoia_field].to_s
|
43
|
-
Mongoid::History.trackable_settings ||= {}
|
44
|
-
Mongoid::History.trackable_settings[name.to_sym] = options
|
45
|
-
end
|
46
|
-
|
47
|
-
def track_history?
|
48
|
-
Mongoid::History.enabled? && Mongoid::History.store[track_history_flag] != false
|
49
|
-
end
|
50
|
-
|
51
|
-
def dynamic_enabled?
|
52
|
-
Mongoid::Compatibility::Version.mongoid3? || (self < Mongoid::Attributes::Dynamic).present?
|
53
|
-
end
|
54
|
-
|
55
|
-
def disable_tracking
|
56
|
-
original_flag = Mongoid::History.store[track_history_flag]
|
57
|
-
Mongoid::History.store[track_history_flag] = false
|
58
|
-
yield if block_given?
|
59
|
-
ensure
|
60
|
-
Mongoid::History.store[track_history_flag] = original_flag if block_given?
|
61
|
-
end
|
62
|
-
|
63
|
-
def enable_tracking
|
64
|
-
original_flag = Mongoid::History.store[track_history_flag]
|
65
|
-
Mongoid::History.store[track_history_flag] = true
|
66
|
-
yield if block_given?
|
67
|
-
ensure
|
68
|
-
Mongoid::History.store[track_history_flag] = original_flag if block_given?
|
69
|
-
end
|
70
|
-
|
71
|
-
alias disable_tracking! disable_tracking
|
72
|
-
alias enable_tracking! enable_tracking
|
73
|
-
|
74
|
-
def track_history_flag
|
75
|
-
"mongoid_history_#{name.underscore}_trackable_enabled".to_sym
|
76
|
-
end
|
77
|
-
|
78
|
-
def tracker_class
|
79
|
-
klass = history_trackable_options[:tracker_class_name] || Mongoid::History.tracker_class_name
|
80
|
-
klass.is_a?(Class) ? klass : klass.to_s.camelize.constantize
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
module MyInstanceMethods
|
85
|
-
def history_tracks
|
86
|
-
@history_tracks ||= self.class.tracker_class.where(
|
87
|
-
scope: related_scope,
|
88
|
-
association_chain: association_hash
|
89
|
-
).asc(:version)
|
90
|
-
end
|
91
|
-
|
92
|
-
# undo :from => 1, :to => 5
|
93
|
-
# undo 4
|
94
|
-
# undo :last => 10
|
95
|
-
def undo(modifier = nil, options_or_version = nil)
|
96
|
-
versions = get_versions_criteria(options_or_version).to_a
|
97
|
-
versions.sort! { |v1, v2| v2.version <=> v1.version }
|
98
|
-
|
99
|
-
versions.each do |v|
|
100
|
-
undo_attr = v.undo_attr(modifier)
|
101
|
-
if Mongoid::Compatibility::Version.mongoid3? # update_attributes! not bypassing rails 3 protected attributes
|
102
|
-
assign_attributes(undo_attr, without_protection: true)
|
103
|
-
else # assign_attributes with 'without_protection' option does not work with rails 4/mongoid 4
|
104
|
-
self.attributes = undo_attr
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# undo! :from => 1, :to => 5
|
110
|
-
# undo! 4
|
111
|
-
# undo! :last => 10
|
112
|
-
def undo!(modifier = nil, options_or_version = nil)
|
113
|
-
undo(modifier, options_or_version)
|
114
|
-
save!
|
115
|
-
end
|
116
|
-
|
117
|
-
def redo!(modifier = nil, options_or_version = nil)
|
118
|
-
versions = get_versions_criteria(options_or_version).to_a
|
119
|
-
versions.sort! { |v1, v2| v1.version <=> v2.version }
|
120
|
-
|
121
|
-
versions.each do |v|
|
122
|
-
redo_attr = v.redo_attr(modifier)
|
123
|
-
if Mongoid::Compatibility::Version.mongoid3?
|
124
|
-
assign_attributes(redo_attr, without_protection: true)
|
125
|
-
save!
|
126
|
-
else
|
127
|
-
update_attributes!(redo_attr)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def _get_relation(name)
|
133
|
-
send(self.class.relation_alias(name))
|
134
|
-
end
|
135
|
-
|
136
|
-
def _create_relation(name, value)
|
137
|
-
send("create_#{self.class.relation_alias(name)}!", value)
|
138
|
-
end
|
139
|
-
|
140
|
-
private
|
141
|
-
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
scope =
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
#
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
relation.class_name == node.
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
#
|
218
|
-
#
|
219
|
-
#
|
220
|
-
# @
|
221
|
-
#
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
@history_tracker_attributes
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
@modified_attributes_for_update = nil
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
#
|
279
|
-
#
|
280
|
-
#
|
281
|
-
#
|
282
|
-
#
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
#
|
297
|
-
#
|
298
|
-
#
|
299
|
-
#
|
300
|
-
#
|
301
|
-
#
|
302
|
-
#
|
303
|
-
# @param [
|
304
|
-
#
|
305
|
-
#
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
#
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
#
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
#
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
#
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
#
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
#
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
#
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
#
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
@history_trackable_options
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
@
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
end
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
1
|
+
module Mongoid
|
2
|
+
module History
|
3
|
+
module Trackable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def track_history(options = {})
|
8
|
+
extend RelationMethods
|
9
|
+
|
10
|
+
history_options = Mongoid::History::Options.new(self, options)
|
11
|
+
|
12
|
+
field history_options.options[:version_field].to_sym, type: Integer
|
13
|
+
|
14
|
+
unless history_options.options[:modifier_field].nil?
|
15
|
+
belongs_to_modifier_options = { class_name: Mongoid::History.modifier_class_name }
|
16
|
+
belongs_to_modifier_options[:inverse_of] = history_options.options[:modifier_field_inverse_of] if history_options.options.key?(:modifier_field_inverse_of)
|
17
|
+
belongs_to_modifier_options[:optional] = true if history_options.options[:modifier_field_optional] && Mongoid::Compatibility::Version.mongoid6_or_newer?
|
18
|
+
belongs_to history_options.options[:modifier_field].to_sym, belongs_to_modifier_options
|
19
|
+
end
|
20
|
+
|
21
|
+
include MyInstanceMethods
|
22
|
+
extend SingletonMethods
|
23
|
+
|
24
|
+
delegate :history_trackable_options, to: 'self.class'
|
25
|
+
delegate :track_history?, to: 'self.class'
|
26
|
+
|
27
|
+
callback_options = history_options.options.slice(:if, :unless)
|
28
|
+
around_update :track_update, **callback_options if history_options.options[:track_update]
|
29
|
+
around_create :track_create, **callback_options if history_options.options[:track_create]
|
30
|
+
around_destroy :track_destroy, **callback_options if history_options.options[:track_destroy]
|
31
|
+
|
32
|
+
unless respond_to? :mongoid_history_options
|
33
|
+
class_attribute :mongoid_history_options, instance_accessor: false
|
34
|
+
end
|
35
|
+
|
36
|
+
self.mongoid_history_options = history_options
|
37
|
+
end
|
38
|
+
|
39
|
+
def history_settings(options = {})
|
40
|
+
options = Mongoid::History.default_settings.merge(options.symbolize_keys)
|
41
|
+
options = options.slice(*Mongoid::History.default_settings.keys)
|
42
|
+
options[:paranoia_field] = aliased_fields[options[:paranoia_field].to_s] || options[:paranoia_field].to_s
|
43
|
+
Mongoid::History.trackable_settings ||= {}
|
44
|
+
Mongoid::History.trackable_settings[name.to_sym] = options
|
45
|
+
end
|
46
|
+
|
47
|
+
def track_history?
|
48
|
+
Mongoid::History.enabled? && Mongoid::History.store[track_history_flag] != false
|
49
|
+
end
|
50
|
+
|
51
|
+
def dynamic_enabled?
|
52
|
+
Mongoid::Compatibility::Version.mongoid3? || (self < Mongoid::Attributes::Dynamic).present?
|
53
|
+
end
|
54
|
+
|
55
|
+
def disable_tracking
|
56
|
+
original_flag = Mongoid::History.store[track_history_flag]
|
57
|
+
Mongoid::History.store[track_history_flag] = false
|
58
|
+
yield if block_given?
|
59
|
+
ensure
|
60
|
+
Mongoid::History.store[track_history_flag] = original_flag if block_given?
|
61
|
+
end
|
62
|
+
|
63
|
+
def enable_tracking
|
64
|
+
original_flag = Mongoid::History.store[track_history_flag]
|
65
|
+
Mongoid::History.store[track_history_flag] = true
|
66
|
+
yield if block_given?
|
67
|
+
ensure
|
68
|
+
Mongoid::History.store[track_history_flag] = original_flag if block_given?
|
69
|
+
end
|
70
|
+
|
71
|
+
alias disable_tracking! disable_tracking
|
72
|
+
alias enable_tracking! enable_tracking
|
73
|
+
|
74
|
+
def track_history_flag
|
75
|
+
"mongoid_history_#{name.underscore}_trackable_enabled".to_sym
|
76
|
+
end
|
77
|
+
|
78
|
+
def tracker_class
|
79
|
+
klass = history_trackable_options[:tracker_class_name] || Mongoid::History.tracker_class_name
|
80
|
+
klass.is_a?(Class) ? klass : klass.to_s.camelize.constantize
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module MyInstanceMethods
|
85
|
+
def history_tracks
|
86
|
+
@history_tracks ||= self.class.tracker_class.where(
|
87
|
+
scope: related_scope,
|
88
|
+
association_chain: association_hash
|
89
|
+
).asc(:version)
|
90
|
+
end
|
91
|
+
|
92
|
+
# undo :from => 1, :to => 5
|
93
|
+
# undo 4
|
94
|
+
# undo :last => 10
|
95
|
+
def undo(modifier = nil, options_or_version = nil)
|
96
|
+
versions = get_versions_criteria(options_or_version).to_a
|
97
|
+
versions.sort! { |v1, v2| v2.version <=> v1.version }
|
98
|
+
|
99
|
+
versions.each do |v|
|
100
|
+
undo_attr = v.undo_attr(modifier)
|
101
|
+
if Mongoid::Compatibility::Version.mongoid3? # update_attributes! not bypassing rails 3 protected attributes
|
102
|
+
assign_attributes(undo_attr, without_protection: true)
|
103
|
+
else # assign_attributes with 'without_protection' option does not work with rails 4/mongoid 4
|
104
|
+
self.attributes = undo_attr
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# undo! :from => 1, :to => 5
|
110
|
+
# undo! 4
|
111
|
+
# undo! :last => 10
|
112
|
+
def undo!(modifier = nil, options_or_version = nil)
|
113
|
+
undo(modifier, options_or_version)
|
114
|
+
save!
|
115
|
+
end
|
116
|
+
|
117
|
+
def redo!(modifier = nil, options_or_version = nil)
|
118
|
+
versions = get_versions_criteria(options_or_version).to_a
|
119
|
+
versions.sort! { |v1, v2| v1.version <=> v2.version }
|
120
|
+
|
121
|
+
versions.each do |v|
|
122
|
+
redo_attr = v.redo_attr(modifier)
|
123
|
+
if Mongoid::Compatibility::Version.mongoid3?
|
124
|
+
assign_attributes(redo_attr, without_protection: true)
|
125
|
+
save!
|
126
|
+
else
|
127
|
+
update_attributes!(redo_attr)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def _get_relation(name)
|
133
|
+
send(self.class.relation_alias(name))
|
134
|
+
end
|
135
|
+
|
136
|
+
def _create_relation(name, value)
|
137
|
+
send("create_#{self.class.relation_alias(name)}!", value)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def ancestor_flagged_for_destroy?(doc)
|
143
|
+
doc && (doc.flagged_for_destroy? || ancestor_flagged_for_destroy?(doc._parent))
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_versions_criteria(options_or_version)
|
147
|
+
if options_or_version.is_a? Hash
|
148
|
+
options = options_or_version
|
149
|
+
if options[:from] && options[:to]
|
150
|
+
lower = options[:from] >= options[:to] ? options[:to] : options[:from]
|
151
|
+
upper = options[:from] < options[:to] ? options[:to] : options[:from]
|
152
|
+
versions = history_tracks.where(:version.in => (lower..upper).to_a)
|
153
|
+
elsif options[:last]
|
154
|
+
versions = history_tracks.limit(options[:last])
|
155
|
+
else
|
156
|
+
raise 'Invalid options, please specify (:from / :to) keys or :last key.'
|
157
|
+
end
|
158
|
+
else
|
159
|
+
options_or_version = options_or_version.to_a if options_or_version.is_a?(Range)
|
160
|
+
version_field_name = history_trackable_options[:version_field]
|
161
|
+
version = options_or_version || attributes[version_field_name] || attributes[version_field_name.to_s]
|
162
|
+
version = [version].flatten
|
163
|
+
versions = history_tracks.where(:version.in => version)
|
164
|
+
end
|
165
|
+
versions.desc(:version)
|
166
|
+
end
|
167
|
+
|
168
|
+
def related_scope
|
169
|
+
scope = history_trackable_options[:scope]
|
170
|
+
|
171
|
+
# Use top level document if its name is specified in the scope
|
172
|
+
root_document_name = traverse_association_chain.first['name'].singularize.underscore.tr('/', '_').to_sym
|
173
|
+
if scope.is_a?(Array) && scope.include?(root_document_name)
|
174
|
+
scope = root_document_name
|
175
|
+
else
|
176
|
+
scope = _parent.collection_name.to_s.singularize.to_sym if scope.is_a?(Array)
|
177
|
+
if Mongoid::Compatibility::Version.mongoid3?
|
178
|
+
scope = metadata.inverse_class_name.tableize.singularize.to_sym if metadata.present? && scope == metadata.as
|
179
|
+
elsif Mongoid::Compatibility::Version.mongoid6_or_older?
|
180
|
+
scope = relation_metadata.inverse_class_name.tableize.singularize.to_sym if relation_metadata.present? && scope == relation_metadata.as
|
181
|
+
elsif Mongoid::Compatibility::Version.mongoid7_or_newer?
|
182
|
+
scope = _association.inverse_class_name.tableize.singularize.to_sym if _association.present? && scope == _association.as
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
scope
|
187
|
+
end
|
188
|
+
|
189
|
+
def traverse_association_chain(node = self)
|
190
|
+
(node._parent ? traverse_association_chain(node._parent) : []).tap { |list| list << association_hash(node) }
|
191
|
+
end
|
192
|
+
|
193
|
+
def association_hash(node = self)
|
194
|
+
# We prefer to look up associations through the parent record because
|
195
|
+
# we're assured, through the object creation, it'll exist. Whereas we're not guaranteed
|
196
|
+
# the child to parent (embedded_in, belongs_to) relation will be defined
|
197
|
+
if node._parent
|
198
|
+
meta = node._parent.relations.values.find do |relation|
|
199
|
+
if Mongoid::Compatibility::Version.mongoid3?
|
200
|
+
relation.class_name == node.metadata.class_name.to_s && relation.name == node.metadata.name
|
201
|
+
elsif Mongoid::Compatibility::Version.mongoid6_or_older?
|
202
|
+
relation.class_name == node.relation_metadata.class_name.to_s &&
|
203
|
+
relation.name == node.relation_metadata.name
|
204
|
+
elsif Mongoid::Compatibility::Version.mongoid7_or_newer?
|
205
|
+
relation.class_name == node._association.class_name.to_s &&
|
206
|
+
relation.name == node._association.name
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# if root node has no meta, and should use class name instead
|
212
|
+
name = meta ? meta.key.to_s : node.class.name
|
213
|
+
|
214
|
+
ActiveSupport::OrderedHash['name', name, 'id', node.id]
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns a Hash of field name to pairs of original and modified values
|
218
|
+
# for each tracked field for a given action.
|
219
|
+
#
|
220
|
+
# @param [ String | Symbol ] action The modification action (:create, :update, :destroy)
|
221
|
+
#
|
222
|
+
# @return [ Hash<String, Array<Object>> ] the pairs of original and modified
|
223
|
+
# values for each field
|
224
|
+
def modified_attributes_for_action(action)
|
225
|
+
case action.to_sym
|
226
|
+
when :destroy then modified_attributes_for_destroy
|
227
|
+
when :create then modified_attributes_for_create
|
228
|
+
else modified_attributes_for_update
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def modified_attributes_for_update
|
233
|
+
@modified_attributes_for_update ||= Mongoid::History::Attributes::Update.new(self).attributes
|
234
|
+
end
|
235
|
+
|
236
|
+
def modified_attributes_for_create
|
237
|
+
@modified_attributes_for_create ||= Mongoid::History::Attributes::Create.new(self).attributes
|
238
|
+
end
|
239
|
+
|
240
|
+
def modified_attributes_for_destroy
|
241
|
+
@modified_attributes_for_destroy ||= Mongoid::History::Attributes::Destroy.new(self).attributes
|
242
|
+
end
|
243
|
+
|
244
|
+
def history_tracker_attributes(action)
|
245
|
+
return @history_tracker_attributes if @history_tracker_attributes
|
246
|
+
|
247
|
+
modifier_field = history_trackable_options[:modifier_field]
|
248
|
+
@history_tracker_attributes = {
|
249
|
+
association_chain: traverse_association_chain,
|
250
|
+
scope: related_scope
|
251
|
+
}
|
252
|
+
@history_tracker_attributes[:modifier] = send(modifier_field) if modifier_field
|
253
|
+
|
254
|
+
original, modified = transform_changes(modified_attributes_for_action(action))
|
255
|
+
|
256
|
+
@history_tracker_attributes[:original] = original
|
257
|
+
@history_tracker_attributes[:modified] = modified
|
258
|
+
@history_tracker_attributes
|
259
|
+
end
|
260
|
+
|
261
|
+
def track_create(&block)
|
262
|
+
track_history_for_action(:create, &block)
|
263
|
+
end
|
264
|
+
|
265
|
+
def track_update(&block)
|
266
|
+
track_history_for_action(:update, &block)
|
267
|
+
end
|
268
|
+
|
269
|
+
def track_destroy(&block)
|
270
|
+
track_history_for_action(:destroy, &block) unless destroyed?
|
271
|
+
end
|
272
|
+
|
273
|
+
def clear_trackable_memoization
|
274
|
+
@history_tracker_attributes = @modified_attributes_for_create = @modified_attributes_for_update = @history_tracks = nil
|
275
|
+
end
|
276
|
+
|
277
|
+
# Transform hash of pair of changes into an `original` and `modified` hash
|
278
|
+
# Nested document keys (key name with dots) are expanded
|
279
|
+
#
|
280
|
+
# @param [Hash<Array>] changes
|
281
|
+
#
|
282
|
+
# @return [Array<Hash<?>,Hash<?>>] <description>
|
283
|
+
def transform_changes(changes)
|
284
|
+
original = {}
|
285
|
+
modified = {}
|
286
|
+
changes.each_pair do |k, modification_pair|
|
287
|
+
o, m = modification_pair
|
288
|
+
original.deep_merge!(expand_nested_document_key_value(k, o)) unless o.nil?
|
289
|
+
modified.deep_merge!(expand_nested_document_key_value(k, m)) unless m.nil?
|
290
|
+
end
|
291
|
+
|
292
|
+
[original, modified]
|
293
|
+
end
|
294
|
+
|
295
|
+
# Handle nested document tracking of changes
|
296
|
+
#
|
297
|
+
# @example
|
298
|
+
#
|
299
|
+
# expand_nested_document_key('embedded.document.changed_field', 'old'])
|
300
|
+
# #=> { 'embedded' => {'document' => { 'changed_field' => 'old' }}}
|
301
|
+
#
|
302
|
+
# @param [String] document_key key with dots
|
303
|
+
# @param [?] value
|
304
|
+
#
|
305
|
+
# @return [Hash<String, ?>]
|
306
|
+
def expand_nested_document_key_value(document_key, value)
|
307
|
+
expanded_key = value
|
308
|
+
document_key.to_s.split('.').reverse.each do |key|
|
309
|
+
expanded_key = { key => expanded_key }
|
310
|
+
end
|
311
|
+
expanded_key
|
312
|
+
end
|
313
|
+
|
314
|
+
def next_version
|
315
|
+
(send(history_trackable_options[:version_field]) || 0) + 1
|
316
|
+
end
|
317
|
+
|
318
|
+
def increment_current_version
|
319
|
+
next_version.tap { |version| send("#{history_trackable_options[:version_field]}=", version) }
|
320
|
+
end
|
321
|
+
|
322
|
+
protected
|
323
|
+
|
324
|
+
def track_history_for_action?(action)
|
325
|
+
track_history? && !(action.to_sym == :update && modified_attributes_for_update.blank?)
|
326
|
+
end
|
327
|
+
|
328
|
+
def increment_current_version?(action)
|
329
|
+
action != :destroy && !ancestor_flagged_for_destroy?(_parent)
|
330
|
+
end
|
331
|
+
|
332
|
+
def track_history_for_action(action)
|
333
|
+
if track_history_for_action?(action)
|
334
|
+
current_version = increment_current_version?(action) ? increment_current_version : next_version
|
335
|
+
last_track = self.class.tracker_class.create!(
|
336
|
+
history_tracker_attributes(action.to_sym)
|
337
|
+
.merge(version: current_version, action: action.to_s, trackable: self)
|
338
|
+
)
|
339
|
+
end
|
340
|
+
|
341
|
+
clear_trackable_memoization
|
342
|
+
|
343
|
+
begin
|
344
|
+
yield
|
345
|
+
rescue => e
|
346
|
+
if track_history_for_action?(action)
|
347
|
+
send("#{history_trackable_options[:version_field]}=", current_version - 1)
|
348
|
+
last_track.destroy
|
349
|
+
end
|
350
|
+
raise e
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
module RelationMethods
|
356
|
+
# Returns a relation class for the given field.
|
357
|
+
#
|
358
|
+
# @param [ String | Symbol ] field The name of the field.
|
359
|
+
#
|
360
|
+
# @return [ nil | Constant ] Class being related.
|
361
|
+
def relation_class_of(field)
|
362
|
+
meta = meta_of(field)
|
363
|
+
return meta.class_name.constantize if meta
|
364
|
+
end
|
365
|
+
|
366
|
+
# Indicates whether there is an Embedded::One relation for the given embedded field.
|
367
|
+
#
|
368
|
+
# @param [ String | Symbol ] embed The name of the embedded field.
|
369
|
+
#
|
370
|
+
# @return [ Boolean ] true if there is an Embedded::One relation for the given embedded field.
|
371
|
+
def embeds_one?(field)
|
372
|
+
relation_of(field) == if Mongoid::Compatibility::Version.mongoid7_or_newer?
|
373
|
+
Mongoid::Association::Embedded::EmbedsOne::Proxy
|
374
|
+
else
|
375
|
+
Mongoid::Relations::Embedded::One
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Indicates whether there is an Embedded::Many relation for the given embedded field.
|
380
|
+
#
|
381
|
+
# @param [ String | Symbol ] field The name of the embedded field.
|
382
|
+
#
|
383
|
+
# @return [ Boolean ] true if there is an Embedded::Many relation for the given embedded field.
|
384
|
+
def embeds_many?(field)
|
385
|
+
relation_of(field) == if Mongoid::Compatibility::Version.mongoid7_or_newer?
|
386
|
+
Mongoid::Association::Embedded::EmbedsMany::Proxy
|
387
|
+
else
|
388
|
+
Mongoid::Relations::Embedded::Many
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Retrieves the database representation of an embedded field name, in case the :store_as option is used.
|
393
|
+
#
|
394
|
+
# @param [ String | Symbol ] embed The name or alias of the embedded field.
|
395
|
+
#
|
396
|
+
# @return [ String ] The database name of the embedded field.
|
397
|
+
def relation_alias(embed)
|
398
|
+
relation_aliases[embed]
|
399
|
+
end
|
400
|
+
|
401
|
+
protected
|
402
|
+
|
403
|
+
# Return the reflected metadata for a relation.
|
404
|
+
#
|
405
|
+
# @param [ String ] field The database field name for a relation.
|
406
|
+
#
|
407
|
+
# @return [ nil | Mongoid::Relations::Metadata ]
|
408
|
+
def meta_of(field)
|
409
|
+
@meta_of ||= {}
|
410
|
+
return @meta_of[field] if @meta_of.key?(field)
|
411
|
+
@meta_of[field] = reflect_on_association(relation_alias(field))
|
412
|
+
end
|
413
|
+
|
414
|
+
# Returns a relation for the given field.
|
415
|
+
#
|
416
|
+
# @param [ String | Symbol ] field The name of the field.
|
417
|
+
#
|
418
|
+
# @return [ nil | Constant ] Type of relation.
|
419
|
+
def relation_of(field)
|
420
|
+
meta = meta_of(field)
|
421
|
+
meta ? meta.relation : nil
|
422
|
+
end
|
423
|
+
|
424
|
+
# Retrieves the memoized hash of embedded aliases and their associated database representations.
|
425
|
+
#
|
426
|
+
# @return [ Hash < String, String > ] hash of embedded aliases (keys) to database representations (values)
|
427
|
+
def relation_aliases
|
428
|
+
@relation_aliases ||= relations.inject(HashWithIndifferentAccess.new) do |h, (k, v)|
|
429
|
+
store_as = Mongoid::Compatibility::Version.mongoid7_or_newer? ? v.store_as : v[:store_as]
|
430
|
+
h[store_as || k] = k
|
431
|
+
h
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
module SingletonMethods
|
437
|
+
# Whether or not the field or embedded relation should be tracked.
|
438
|
+
#
|
439
|
+
# @param [ String | Symbol ] field_or_relation The name or alias of the field OR the name of embedded relation
|
440
|
+
# @param [ String | Symbol ] action The optional action name (:create, :update, or :destroy)
|
441
|
+
#
|
442
|
+
# @return [ Boolean ] whether or not the field or embedded relation is tracked for the given action
|
443
|
+
def tracked?(field_or_relation, action = :update)
|
444
|
+
tracked_field?(field_or_relation, action) || tracked_relation?(field_or_relation)
|
445
|
+
end
|
446
|
+
|
447
|
+
# Whether or not the field should be tracked.
|
448
|
+
#
|
449
|
+
# @param [ String | Symbol ] field The name or alias of the field
|
450
|
+
# @param [ String | Symbol ] action The optional action name (:create, :update, or :destroy)
|
451
|
+
#
|
452
|
+
# @return [ Boolean ] whether or not the field is tracked for the given action
|
453
|
+
def tracked_field?(field, action = :update)
|
454
|
+
dynamic_field?(field) || tracked_fields_for_action(action).include?(database_field_name(field))
|
455
|
+
end
|
456
|
+
|
457
|
+
# Checks if field is dynamic.
|
458
|
+
#
|
459
|
+
# @param [ String | Symbol ] field The name of the dynamic field
|
460
|
+
#
|
461
|
+
# @return [ Boolean ] whether or not the field is dynamic
|
462
|
+
def dynamic_field?(field)
|
463
|
+
dynamic_enabled? &&
|
464
|
+
!fields.keys.include?(database_field_name(field)) &&
|
465
|
+
!embedded_relations.map { |_, v| v.key }.include?(database_field_name(field))
|
466
|
+
end
|
467
|
+
|
468
|
+
def field_format(field)
|
469
|
+
field_formats[database_field_name(field)]
|
470
|
+
end
|
471
|
+
|
472
|
+
# Retrieves the list of tracked fields for a given action.
|
473
|
+
#
|
474
|
+
# @param [ String | Symbol ] action The action name (:create, :update, or :destroy)
|
475
|
+
#
|
476
|
+
# @return [ Array < String > ] the list of tracked fields for the given action
|
477
|
+
def tracked_fields_for_action(action)
|
478
|
+
case action.to_sym
|
479
|
+
when :destroy then tracked_fields + reserved_tracked_fields
|
480
|
+
else tracked_fields
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Retrieves the memoized base list of tracked fields, excluding reserved fields.
|
485
|
+
#
|
486
|
+
# @return [ Array < String > ] the base list of tracked database field names
|
487
|
+
def tracked_fields
|
488
|
+
@tracked_fields ||= history_trackable_options[:fields] + history_trackable_options[:dynamic]
|
489
|
+
end
|
490
|
+
|
491
|
+
# Retrieves the memoized list of reserved tracked fields, which are only included for certain actions.
|
492
|
+
#
|
493
|
+
# @return [ Array < String > ] the list of reserved database field names
|
494
|
+
def reserved_tracked_fields
|
495
|
+
@reserved_tracked_fields ||= begin
|
496
|
+
fields = ['_id', history_trackable_options[:version_field].to_s]
|
497
|
+
modifier_field = history_trackable_options[:modifier_field]
|
498
|
+
fields << "#{modifier_field}_id" if modifier_field
|
499
|
+
fields
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def field_formats
|
504
|
+
@field_formats ||= history_trackable_options[:format]
|
505
|
+
end
|
506
|
+
|
507
|
+
# Whether or not the relation should be tracked.
|
508
|
+
#
|
509
|
+
# @param [ String | Symbol ] relation The name of the relation
|
510
|
+
#
|
511
|
+
# @return [ Boolean ] whether or not the relation is tracked
|
512
|
+
def tracked_relation?(relation)
|
513
|
+
tracked_embeds_one?(relation) || tracked_embeds_many?(relation)
|
514
|
+
end
|
515
|
+
|
516
|
+
# Whether or not the embeds_one relation should be tracked.
|
517
|
+
#
|
518
|
+
# @param [ String | Symbol ] relation The name of the embeds_one relation
|
519
|
+
#
|
520
|
+
# @return [ Boolean ] whether or not the embeds_one relation is tracked
|
521
|
+
def tracked_embeds_one?(relation)
|
522
|
+
tracked_embeds_one.include?(database_field_name(relation))
|
523
|
+
end
|
524
|
+
|
525
|
+
# Retrieves the memoized list of tracked embeds_one relations
|
526
|
+
#
|
527
|
+
# @return [ Array < String > ] the list of tracked embeds_one relations
|
528
|
+
def tracked_embeds_one
|
529
|
+
@tracked_embeds_one ||= begin
|
530
|
+
reflect_on_all_associations(:embeds_one)
|
531
|
+
.map(&:key)
|
532
|
+
.select { |rel| history_trackable_options[:relations][:embeds_one].include? rel }
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
def tracked_embeds_one_attributes(relation)
|
537
|
+
history_trackable_options[:relations][:embeds_one][database_field_name(relation)]
|
538
|
+
end
|
539
|
+
|
540
|
+
# Whether or not the embeds_many relation should be tracked.
|
541
|
+
#
|
542
|
+
# @param [ String | Symbol ] relation The name of the embeds_many relation
|
543
|
+
#
|
544
|
+
# @return [ Boolean ] whether or not the embeds_many relation is tracked
|
545
|
+
def tracked_embeds_many?(relation)
|
546
|
+
tracked_embeds_many.include?(database_field_name(relation))
|
547
|
+
end
|
548
|
+
|
549
|
+
# Retrieves the memoized list of tracked embeds_many relations
|
550
|
+
#
|
551
|
+
# @return [ Array < String > ] the list of tracked embeds_many relations
|
552
|
+
def tracked_embeds_many
|
553
|
+
@tracked_embeds_many ||= begin
|
554
|
+
reflect_on_all_associations(:embeds_many)
|
555
|
+
.map(&:key)
|
556
|
+
.select { |rel| history_trackable_options[:relations][:embeds_many].include? rel }
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def tracked_embeds_many_attributes(relation)
|
561
|
+
history_trackable_options[:relations][:embeds_many][database_field_name(relation)]
|
562
|
+
end
|
563
|
+
|
564
|
+
def trackable_scope
|
565
|
+
collection_name.to_s.singularize.to_sym
|
566
|
+
end
|
567
|
+
|
568
|
+
def history_trackable_options
|
569
|
+
@history_trackable_options ||= mongoid_history_options.prepared
|
570
|
+
end
|
571
|
+
|
572
|
+
def clear_trackable_memoization
|
573
|
+
@reserved_tracked_fields = nil
|
574
|
+
@history_trackable_options = nil
|
575
|
+
@trackable_settings = nil
|
576
|
+
@tracked_fields = nil
|
577
|
+
@tracked_embeds_one = nil
|
578
|
+
@tracked_embeds_many = nil
|
579
|
+
end
|
580
|
+
|
581
|
+
def inherited(subclass)
|
582
|
+
super
|
583
|
+
subclass.mongoid_history_options = Mongoid::History::Options.new(subclass, mongoid_history_options.options)
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|