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