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