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
data/README.md
CHANGED
@@ -1,608 +1,609 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
[![
|
5
|
-
[![
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
class
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
default
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
```
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
include Mongoid::
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
:
|
73
|
-
:
|
74
|
-
:
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
include Mongoid::
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
#
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
comment.
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
track
|
119
|
-
|
120
|
-
#
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
post
|
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
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
```
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
```
|
200
|
-
|
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
|
-
post
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
comment.
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
-
# get
|
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
|
-
Changed field #{k} by adding #{v[:add]}
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
class
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
If your app
|
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
|
-
end
|
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
|
-
end
|
564
|
-
|
565
|
-
class
|
566
|
-
include Mongoid::History::Tracker
|
567
|
-
end
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
1
|
+
# Mongoid History
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/mongoid-history)
|
4
|
+
[](https://github.com/mongoid/mongoid-history/actions/workflows/test.yml?query=branch%3Amaster)
|
5
|
+
[](https://codeclimate.com/github/mongoid/mongoid-history)
|
6
|
+
|
7
|
+
Mongoid History tracks historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. Embedded documents are referenced by storing an association path, which is an array of `document_name` and `document_id` fields starting from the top most parent document and down to the embedded document that should track history.
|
8
|
+
|
9
|
+
This gem also implements multi-user undo, which allows users to undo any history change in any order. Undoing a document also creates a new history track. This is great for auditing and preventing vandalism, but is probably not suitable for use cases such as a wiki (but we won't stop you either).
|
10
|
+
|
11
|
+
### Version Support
|
12
|
+
|
13
|
+
Mongoid History supports the following dependency versions:
|
14
|
+
|
15
|
+
* Ruby 2.3+
|
16
|
+
* Mongoid 3.1+
|
17
|
+
* Recent JRuby versions
|
18
|
+
|
19
|
+
Earlier Ruby versions may work but are untested.
|
20
|
+
|
21
|
+
## Install
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'mongoid-history'
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Create a history tracker
|
30
|
+
|
31
|
+
Create a new class to track histories. All histories are stored in this tracker. The name of the class can be anything you like. The only requirement is that it includes `Mongoid::History::Tracker`
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# app/models/history_tracker.rb
|
35
|
+
class HistoryTracker
|
36
|
+
include Mongoid::History::Tracker
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
### Set default tracker class name (optional)
|
41
|
+
|
42
|
+
Mongoid::History will use the first loaded class to include Mongoid::History::Tracker as the
|
43
|
+
default history tracker. If you are using multiple Tracker classes, you should set a global
|
44
|
+
default in a Rails initializer:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# config/initializers/mongoid_history.rb
|
48
|
+
# initializer for mongoid-history
|
49
|
+
# assuming HistoryTracker is your tracker class
|
50
|
+
Mongoid::History.tracker_class_name = :history_tracker
|
51
|
+
```
|
52
|
+
|
53
|
+
### Create trackable classes and objects
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Post
|
57
|
+
include Mongoid::Document
|
58
|
+
include Mongoid::Timestamps
|
59
|
+
|
60
|
+
# history tracking all Post documents
|
61
|
+
# note: tracking will not work until #track_history is invoked
|
62
|
+
include Mongoid::History::Trackable
|
63
|
+
|
64
|
+
field :title
|
65
|
+
field :body
|
66
|
+
field :rating
|
67
|
+
embeds_many :comments
|
68
|
+
|
69
|
+
# telling Mongoid::History how you want to track changes
|
70
|
+
# dynamic fields will be tracked automatically (for MongoId 4.0+ you should include Mongoid::Attributes::Dynamic to your model)
|
71
|
+
track_history :on => [:title, :body], # track title and body fields only, default is :all
|
72
|
+
:modifier_field => :modifier, # adds "belongs_to :modifier" to track who made the change, default is :modifier, set to nil to not create modifier_field
|
73
|
+
:modifier_field_inverse_of => :nil, # adds an ":inverse_of" option to the "belongs_to :modifier" relation, default is not set
|
74
|
+
:modifier_field_optional => true, # marks the modifier relationship as optional (requires Mongoid 6 or higher)
|
75
|
+
:version_field => :version, # adds "field :version, :type => Integer" to track current version, default is :version
|
76
|
+
:track_create => true, # track document creation, default is true
|
77
|
+
:track_update => true, # track document updates, default is true
|
78
|
+
:track_destroy => true # track document destruction, default is true
|
79
|
+
end
|
80
|
+
|
81
|
+
class Comment
|
82
|
+
include Mongoid::Document
|
83
|
+
include Mongoid::Timestamps
|
84
|
+
|
85
|
+
# declare that we want to track comments
|
86
|
+
include Mongoid::History::Trackable
|
87
|
+
|
88
|
+
field :title
|
89
|
+
field :body
|
90
|
+
embedded_in :post, :inverse_of => :comments
|
91
|
+
|
92
|
+
# track title and body for all comments, scope it to post (the parent)
|
93
|
+
# also track creation and destruction
|
94
|
+
track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true
|
95
|
+
|
96
|
+
# For embedded polymorphic relations, specify an array of model names or its polymorphic name
|
97
|
+
# e.g. :scope => [:post, :image, :video]
|
98
|
+
# :scope => :commentable
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# the modifier class
|
103
|
+
class User
|
104
|
+
include Mongoid::Document
|
105
|
+
include Mongoid::Timestamps
|
106
|
+
|
107
|
+
field :name
|
108
|
+
end
|
109
|
+
|
110
|
+
user = User.create(:name => "Aaron")
|
111
|
+
post = Post.create(:title => "Test", :body => "Post", :modifier => user)
|
112
|
+
comment = post.comments.create(:title => "test", :body => "comment", :modifier => user)
|
113
|
+
comment.history_tracks.count # should be 1
|
114
|
+
|
115
|
+
comment.update_attributes(:title => "Test 2")
|
116
|
+
comment.history_tracks.count # should be 2
|
117
|
+
|
118
|
+
track = comment.history_tracks.last
|
119
|
+
|
120
|
+
track.undo! user # comment title should be "Test"
|
121
|
+
|
122
|
+
track.redo! user # comment title should be "Test 2"
|
123
|
+
|
124
|
+
# undo comment to version 1 without save
|
125
|
+
comment.undo nil, from: 1, to: comment.version
|
126
|
+
|
127
|
+
# undo last change
|
128
|
+
comment.undo! user
|
129
|
+
|
130
|
+
# undo versions 1 - 4
|
131
|
+
comment.undo! user, :from => 4, :to => 1
|
132
|
+
|
133
|
+
# undo last 3 versions
|
134
|
+
comment.undo! user, :last => 3
|
135
|
+
|
136
|
+
# redo versions 1 - 4
|
137
|
+
comment.redo! user, :from => 1, :to => 4
|
138
|
+
|
139
|
+
# redo last 3 versions
|
140
|
+
comment.redo! user, :last => 3
|
141
|
+
|
142
|
+
# redo version 1
|
143
|
+
comment.redo! user, 1
|
144
|
+
|
145
|
+
# delete post
|
146
|
+
post.destroy
|
147
|
+
|
148
|
+
# undelete post
|
149
|
+
post.undo! user
|
150
|
+
|
151
|
+
# disable tracking for comments within a block
|
152
|
+
Comment.disable_tracking do
|
153
|
+
comment.update_attributes(:title => "Test 3")
|
154
|
+
end
|
155
|
+
|
156
|
+
# disable tracking for comments by default
|
157
|
+
Comment.disable_tracking!
|
158
|
+
|
159
|
+
# enable tracking for comments within a block
|
160
|
+
Comment.enable_tracking do
|
161
|
+
comment.update_attributes(:title => "Test 3")
|
162
|
+
end
|
163
|
+
|
164
|
+
# renable tracking for comments by default
|
165
|
+
Comment.enable_tracking!
|
166
|
+
|
167
|
+
# globally disable all history tracking within a block
|
168
|
+
Mongoid::History.disable do
|
169
|
+
comment.update_attributes(:title => "Test 3")
|
170
|
+
user.update_attributes(:name => "Eddie Van Halen")
|
171
|
+
end
|
172
|
+
|
173
|
+
# globally disable all history tracking by default
|
174
|
+
Mongoid::History.disable!
|
175
|
+
|
176
|
+
# globally enable all history tracking within a block
|
177
|
+
Mongoid::History.enable do
|
178
|
+
comment.update_attributes(:title => "Test 3")
|
179
|
+
user.update_attributes(:name => "Eddie Van Halen")
|
180
|
+
end
|
181
|
+
|
182
|
+
# globally renable all history tracking by default
|
183
|
+
Mongoid::History.enable!
|
184
|
+
```
|
185
|
+
|
186
|
+
You may want to track changes on all fields.
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class Post
|
190
|
+
include Mongoid::Document
|
191
|
+
include Mongoid::History::Trackable
|
192
|
+
|
193
|
+
field :title
|
194
|
+
field :body
|
195
|
+
field :rating
|
196
|
+
|
197
|
+
track_history :on => [:fields] # all fields will be tracked
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
You can also track changes on all embedded relations.
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
class Post
|
205
|
+
include Mongoid::Document
|
206
|
+
include Mongoid::History::Trackable
|
207
|
+
|
208
|
+
embeds_many :comments
|
209
|
+
embeds_one :content
|
210
|
+
|
211
|
+
track_history :on => [:embedded_relations] # all embedded relations will be tracked
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
**Include embedded objects attributes in parent audit**
|
216
|
+
|
217
|
+
Modify above `Post` and `Comment` classes as below:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class Post
|
221
|
+
include Mongoid::Document
|
222
|
+
include Mongoid::Timestamps
|
223
|
+
include Mongoid::History::Trackable
|
224
|
+
|
225
|
+
field :title
|
226
|
+
field :body
|
227
|
+
field :rating
|
228
|
+
embeds_many :comments
|
229
|
+
|
230
|
+
track_history :on => [:title, :body, :comments],
|
231
|
+
:modifier_field => :modifier,
|
232
|
+
:modifier_field_inverse_of => :nil,
|
233
|
+
:version_field => :version,
|
234
|
+
:track_create => true, # track create on Post
|
235
|
+
:track_update => true,
|
236
|
+
:track_destroy => false
|
237
|
+
end
|
238
|
+
|
239
|
+
class Comment
|
240
|
+
include Mongoid::Document
|
241
|
+
include Mongoid::Timestamps
|
242
|
+
|
243
|
+
field :title
|
244
|
+
field :body
|
245
|
+
embedded_in :post, :inverse_of => :comments
|
246
|
+
end
|
247
|
+
|
248
|
+
user = User.create(:name => "Aaron")
|
249
|
+
post = Post.create(:title => "Test", :body => "Post", :modifier => user)
|
250
|
+
comment = post.comments.build(:title => "test", :body => "comment", :modifier => user)
|
251
|
+
post.save
|
252
|
+
post.history_tracks.count # should be 1
|
253
|
+
|
254
|
+
comment.respond_to?(:history_tracks) # should be false
|
255
|
+
|
256
|
+
track = post.history_tracks.first
|
257
|
+
track.original # {}
|
258
|
+
track.modified # { "title" => "Test", "body" => "Post", "comments" => [{ "_id" => "575fa9e667d827e5ed00000d", "title" => "test", "body" => "comment" }], ... }
|
259
|
+
```
|
260
|
+
|
261
|
+
### Whitelist the tracked attributes of embedded relations
|
262
|
+
|
263
|
+
If you don't want to track all the attributes of embedded relations in parent audit history, you can whitelist the attributes as below:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class Book
|
267
|
+
include Mongoid::Document
|
268
|
+
...
|
269
|
+
embeds_many :pages
|
270
|
+
track_history :on => { :pages => [:title, :content] }
|
271
|
+
end
|
272
|
+
|
273
|
+
class Page
|
274
|
+
include Mongoid::Document
|
275
|
+
...
|
276
|
+
field :number
|
277
|
+
field :title
|
278
|
+
field :subtitle
|
279
|
+
field :content
|
280
|
+
embedded_in :book
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
It will now track only `_id` (Mandatory), `title` and `content` attributes for `pages` relation.
|
285
|
+
|
286
|
+
### Retrieving the list of tracked static and dynamic fields
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
class Book
|
290
|
+
...
|
291
|
+
field :title
|
292
|
+
field :author
|
293
|
+
field :price
|
294
|
+
track_history :on => [:title, :price]
|
295
|
+
end
|
296
|
+
|
297
|
+
Book.tracked_fields #=> ["title", "price"]
|
298
|
+
Book.tracked_field?(:title) #=> true
|
299
|
+
Book.tracked_field?(:author) #=> false
|
300
|
+
```
|
301
|
+
|
302
|
+
### Retrieving the list of tracked relations
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
class Book
|
306
|
+
...
|
307
|
+
track_history :on => [:pages]
|
308
|
+
end
|
309
|
+
|
310
|
+
Book.tracked_relation?(:pages) #=> true
|
311
|
+
Book.tracked_embeds_many #=> ["pages"]
|
312
|
+
Book.tracked_embeds_many?(:pages) #=> true
|
313
|
+
```
|
314
|
+
|
315
|
+
### Skip soft-deleted embedded objects with nested tracking
|
316
|
+
|
317
|
+
Default paranoia field is `deleted_at`. You can use custom field for each class as below:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
class Book
|
321
|
+
include Mongoid::Document
|
322
|
+
include Mongoid::History::Trackable
|
323
|
+
embeds_many :pages
|
324
|
+
track_history on: :pages
|
325
|
+
end
|
326
|
+
|
327
|
+
class Page
|
328
|
+
include Mongoid::Document
|
329
|
+
include Mongoid::History::Trackable
|
330
|
+
...
|
331
|
+
embedded_in :book
|
332
|
+
history_settings paranoia_field: :removed_at
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
This will skip the `page` documents with `removed_at` set to a non-blank value from nested tracking
|
337
|
+
|
338
|
+
### Formatting fields
|
339
|
+
|
340
|
+
You can opt to use a proc or string interpolation to alter attributes being stored on a history record.
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
class Post
|
344
|
+
include Mongoid::Document
|
345
|
+
include Mongoid::History::Trackable
|
346
|
+
|
347
|
+
field :title
|
348
|
+
track_history on: :title,
|
349
|
+
format: { title: ->(t){ t[0..3] } }
|
350
|
+
```
|
351
|
+
|
352
|
+
This also works for fields on an embedded relations.
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
class Book
|
356
|
+
include Mongoid::Document
|
357
|
+
include Mongoid::History::Trackable
|
358
|
+
|
359
|
+
embeds_many :pages
|
360
|
+
track_history on: :pages,
|
361
|
+
format: { pages: { number: 'pg. %d' } }
|
362
|
+
end
|
363
|
+
|
364
|
+
class Page
|
365
|
+
include Mongoid::Document
|
366
|
+
include Mongoid::History::Trackable
|
367
|
+
|
368
|
+
field :number, type: Integer
|
369
|
+
embedded_in :book
|
370
|
+
end
|
371
|
+
```
|
372
|
+
|
373
|
+
### Displaying history trackers as an audit trail
|
374
|
+
|
375
|
+
In your Controller:
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
# Fetch history trackers
|
379
|
+
@trackers = HistoryTracker.limit(25)
|
380
|
+
|
381
|
+
# get change set for the first tracker
|
382
|
+
@changes = @trackers.first.tracked_changes
|
383
|
+
#=> {field: {to: val1, from: val2}}
|
384
|
+
|
385
|
+
# get edit set for the first tracker
|
386
|
+
@edits = @trackers.first.tracked_edits
|
387
|
+
#=> { add: {field: val},
|
388
|
+
# remove: {field: val},
|
389
|
+
# modify: { to: val1, from: val2 },
|
390
|
+
# array: { add: [val2], remove: [val1] } }
|
391
|
+
```
|
392
|
+
|
393
|
+
In your View, you might do something like (example in HAML format):
|
394
|
+
|
395
|
+
```haml
|
396
|
+
%ul.changes
|
397
|
+
- (@edits[:add]||[]).each do |k,v|
|
398
|
+
%li.remove Added field #{k} value #{v}
|
399
|
+
|
400
|
+
- (@edits[:modify]||[]).each do |k,v|
|
401
|
+
%li.modify Changed field #{k} from #{v[:from]} to #{v[:to]}
|
402
|
+
|
403
|
+
- (@edits[:array]||[]).each do |k,v|
|
404
|
+
%li.modify
|
405
|
+
- if v[:remove].nil?
|
406
|
+
Changed field #{k} by adding #{v[:add]}
|
407
|
+
- elsif v[:add].nil?
|
408
|
+
Changed field #{k} by removing #{v[:remove]}
|
409
|
+
- else
|
410
|
+
Changed field #{k} by adding #{v[:add]} and removing #{v[:remove]}
|
411
|
+
|
412
|
+
- (@edits[:remove]||[]).each do |k,v|
|
413
|
+
%li.remove Removed field #{k} (was previously #{v})
|
414
|
+
```
|
415
|
+
|
416
|
+
### Adding Userstamp on History Trackers
|
417
|
+
|
418
|
+
To track the User in the application who created the HistoryTracker, add the
|
419
|
+
[Mongoid::Userstamp gem](https://github.com/tbpro/mongoid_userstamp) to your HistoryTracker class.
|
420
|
+
This will add a field called `created_by` and an accessor `creator` to the model (you can rename these via gem config).
|
421
|
+
|
422
|
+
```
|
423
|
+
class MyHistoryTracker
|
424
|
+
include Mongoid::History::Tracker
|
425
|
+
include Mongoid::Userstamp
|
426
|
+
end
|
427
|
+
```
|
428
|
+
|
429
|
+
### Setting Modifier Class Name
|
430
|
+
|
431
|
+
If your app will track history changes to a user, Mongoid History looks for these modifiers in the ``User`` class by default. If you have named your 'user' accounts differently, you will need to add that to your Mongoid History config:
|
432
|
+
|
433
|
+
The following examples set the modifier class name using a Rails initializer:
|
434
|
+
|
435
|
+
If your app uses a class ``Author``:
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
# config/initializers/mongoid-history.rb
|
439
|
+
# initializer for mongoid-history
|
440
|
+
|
441
|
+
Mongoid::History.modifier_class_name = 'Author'
|
442
|
+
```
|
443
|
+
|
444
|
+
Or perhaps you are namespacing to a module:
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
Mongoid::History.modifier_class_name = 'CMS::Author'
|
448
|
+
```
|
449
|
+
|
450
|
+
### Conditional :if and :unless options
|
451
|
+
|
452
|
+
The `track_history` method supports `:if` and `:unless` options which will skip generating
|
453
|
+
the history tracker unless they are satisfied. These options can take either a method
|
454
|
+
`Symbol` or a `Proc`. They behave identical to how `:if` and `:unless` behave in Rails model callbacks.
|
455
|
+
|
456
|
+
```ruby
|
457
|
+
track_history on: [:ip],
|
458
|
+
if: :should_i_track_history?,
|
459
|
+
unless: ->(obj){ obj.method_to_skip_history }
|
460
|
+
```
|
461
|
+
|
462
|
+
### Using an alternate changes method
|
463
|
+
|
464
|
+
Sometimes you may wish to provide an alternate method for determining which changes should be tracked. For example, if you are using embedded documents
|
465
|
+
and nested attributes, you may wish to write your own changes method that includes changes from the embedded documents.
|
466
|
+
|
467
|
+
Mongoid::History provides an option named `:changes_method` which allows you to do this. It defaults to `:changes`, which is the standard changes method.
|
468
|
+
|
469
|
+
Note: Specify additional fields that are provided with a custom `changes_method` with the `:on` option.. To specify current fields and additional fields, use `fields.keys + [:custom]`
|
470
|
+
|
471
|
+
Example:
|
472
|
+
|
473
|
+
```ruby
|
474
|
+
class Foo
|
475
|
+
include Mongoid::Document
|
476
|
+
include Mongoid::History::Trackable
|
477
|
+
|
478
|
+
attr_accessor :ip
|
479
|
+
|
480
|
+
track_history on: [:ip], changes_method: :my_changes
|
481
|
+
|
482
|
+
def my_changes
|
483
|
+
unless ip.nil?
|
484
|
+
changes.merge(ip: [nil, ip])
|
485
|
+
else
|
486
|
+
changes
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
```
|
491
|
+
|
492
|
+
Example with embedded & nested attributes:
|
493
|
+
|
494
|
+
```ruby
|
495
|
+
class Foo
|
496
|
+
include Mongoid::Document
|
497
|
+
include Mongoid::Timestamps
|
498
|
+
include Mongoid::History::Trackable
|
499
|
+
|
500
|
+
field :bar
|
501
|
+
embeds_one :baz
|
502
|
+
accepts_nested_attributes_for :baz
|
503
|
+
|
504
|
+
# use changes_with_baz to include baz's changes in this document's
|
505
|
+
# history.
|
506
|
+
track_history on: fields.keys + [:baz], changes_method: :changes_with_baz
|
507
|
+
|
508
|
+
def changes_with_baz
|
509
|
+
if baz.changed?
|
510
|
+
changes.merge(baz: summarized_changes(baz))
|
511
|
+
else
|
512
|
+
changes
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
private
|
517
|
+
# This method takes the changes from an embedded doc and formats them
|
518
|
+
# in a summarized way, similar to how the embedded doc appears in the
|
519
|
+
# parent document's attributes
|
520
|
+
def summarized_changes obj
|
521
|
+
obj.changes.keys.map do |field|
|
522
|
+
next unless obj.respond_to?("#{field}_change")
|
523
|
+
[ { field => obj.send("#{field}_change")[0] },
|
524
|
+
{ field => obj.send("#{field}_change")[1] } ]
|
525
|
+
end.compact.transpose.map do |fields|
|
526
|
+
fields.inject({}) {|map,f| map.merge(f)}
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
class Baz
|
532
|
+
include Mongoid::Document
|
533
|
+
include Mongoid::Timestamps
|
534
|
+
|
535
|
+
embedded_in :foo
|
536
|
+
field :value
|
537
|
+
end
|
538
|
+
```
|
539
|
+
|
540
|
+
For more examples, check out [spec/integration/integration_spec.rb](spec/integration/integration_spec.rb).
|
541
|
+
|
542
|
+
### Multiple Trackers
|
543
|
+
|
544
|
+
You can have different trackers for different classes like so.
|
545
|
+
|
546
|
+
``` ruby
|
547
|
+
class First
|
548
|
+
include Mongoid::Document
|
549
|
+
include Mongoid::History::Trackable
|
550
|
+
|
551
|
+
field :text, type: String
|
552
|
+
track_history on: [:text],
|
553
|
+
tracker_class_name: :first_history_tracker
|
554
|
+
end
|
555
|
+
|
556
|
+
class Second
|
557
|
+
include Mongoid::Document
|
558
|
+
include Mongoid::History::Trackable
|
559
|
+
|
560
|
+
field :text, type: String
|
561
|
+
track_history on: [:text],
|
562
|
+
tracker_class_name: :second_history_tracker
|
563
|
+
end
|
564
|
+
|
565
|
+
class FirstHistoryTracker
|
566
|
+
include Mongoid::History::Tracker
|
567
|
+
end
|
568
|
+
|
569
|
+
class SecondHistoryTracker
|
570
|
+
include Mongoid::History::Tracker
|
571
|
+
end
|
572
|
+
```
|
573
|
+
|
574
|
+
Note that if you are using a tracker for an embedded object that is different
|
575
|
+
from the parent's tracker, redos and undos will not work. You have to use the
|
576
|
+
same tracker for these to work across embedded relationships.
|
577
|
+
|
578
|
+
If you are using multiple trackers and the `tracker_class_name` parameter is
|
579
|
+
not specified, Mongoid::History will use the default tracker configured in the
|
580
|
+
initializer file or whatever the first tracker was loaded.
|
581
|
+
|
582
|
+
### Dependent Restrict Associations
|
583
|
+
|
584
|
+
When `dependent: :restrict` is used on an association, a call to `destroy` on
|
585
|
+
the model will raise `Mongoid::Errors::DeleteRestriction` when the dependency
|
586
|
+
is violated. Just be aware that this gem will create a history track document
|
587
|
+
before the `destroy` call and then remove if an error is raised. This applies
|
588
|
+
to all persistence calls: create, update and destroy.
|
589
|
+
|
590
|
+
See [spec/integration/validation_failure_spec.rb](spec/integration/validation_failure_spec.rb)
|
591
|
+
for examples.
|
592
|
+
|
593
|
+
### Thread Safety
|
594
|
+
|
595
|
+
Mongoid::History stores the tracking enable/disable flag in `Thread.current`.
|
596
|
+
If the [RequestStore](https://github.com/steveklabnik/request_store) gem is installed, Mongoid::History
|
597
|
+
will automatically store variables in the `RequestStore.store` instead. RequestStore is recommended
|
598
|
+
for threaded web servers like Thin or Puma.
|
599
|
+
|
600
|
+
|
601
|
+
## Contributing
|
602
|
+
|
603
|
+
You're encouraged to contribute to Mongoid History. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
604
|
+
|
605
|
+
## Copyright
|
606
|
+
|
607
|
+
Copyright (c) 2011-2020 Aaron Qian and Contributors.
|
608
|
+
|
609
|
+
MIT License. See [LICENSE.txt](LICENSE.txt) for further details.
|