mongoid-history 0.8.3 → 0.8.5

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