mongoid-history 0.6.1 → 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 -92
  9. data/CHANGELOG.md +173 -130
  10. data/CONTRIBUTING.md +117 -118
  11. data/Dangerfile +1 -1
  12. data/Gemfile +49 -35
  13. data/LICENSE.txt +20 -20
  14. data/README.md +609 -531
  15. data/RELEASING.md +66 -0
  16. data/Rakefile +24 -24
  17. data/UPGRADING.md +53 -0
  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 -43
  22. data/lib/mongoid/history/options.rb +177 -184
  23. data/lib/mongoid/history/trackable.rb +588 -501
  24. data/lib/mongoid/history/tracker.rb +247 -238
  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 -936
  33. data/spec/integration/multi_relation_spec.rb +47 -53
  34. data/spec/integration/multiple_trackers_spec.rb +68 -0
  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 -0
  40. data/spec/integration/validation_failure_spec.rb +76 -0
  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 -210
  49. data/spec/unit/callback_options_spec.rb +165 -0
  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 -326
  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 -586
  58. data/spec/unit/tracker_spec.rb +190 -163
  59. metadata +25 -10
  60. data/.travis.yml +0 -35
data/README.md CHANGED
@@ -1,531 +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 and would like to set
41
- a global default you may do so 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
70
- :modifier_field_inverse_of => :nil, # adds an ":inverse_of" option to the "belongs_to :modifier" relation, default is not set
71
- :version_field => :version, # adds "field :version, :type => Integer" to track current version, default is :version
72
- :track_create => false, # track document creation, default is false
73
- :track_update => true, # track document updates, default is true
74
- :track_destroy => false # track document destruction, default is false
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
- # globally disable all history tracking
153
- Mongoid::History.disable do
154
- comment.update_attributes(:title => "Test 3")
155
- user.update_attributes(:name => "Eddie Van Halen")
156
- end
157
- ```
158
-
159
- You may want to track changes on all fields.
160
-
161
- ```ruby
162
- class Post
163
- include Mongoid::Document
164
- include Mongoid::History::Trackable
165
-
166
- field :title
167
- field :body
168
- field :rating
169
-
170
- track_history :on => [:fields] # all fields will be tracked
171
- end
172
- ```
173
-
174
- You can also track changes on all embedded relations.
175
-
176
- ```ruby
177
- class Post
178
- include Mongoid::Document
179
- include Mongoid::History::Trackable
180
-
181
- embeds_many :comments
182
- embeds_one :content
183
-
184
- track_history :on => [:embedded_relations] # all embedded relations will be tracked
185
- end
186
- ```
187
-
188
- **Include embedded objects attributes in parent audit**
189
-
190
- Modify above `Post` and `Comment` classes as below:
191
-
192
- ```ruby
193
- class Post
194
- include Mongoid::Document
195
- include Mongoid::Timestamps
196
- include Mongoid::History::Trackable
197
-
198
- field :title
199
- field :body
200
- field :rating
201
- embeds_many :comments
202
-
203
- track_history :on => [:title, :body, :comments],
204
- :modifier_field => :modifier,
205
- :modifier_field_inverse_of => :nil,
206
- :version_field => :version,
207
- :track_create => true, # track create on Post
208
- :track_update => true,
209
- :track_destroy => false
210
- end
211
-
212
- class Comment
213
- include Mongoid::Document
214
- include Mongoid::Timestamps
215
-
216
- field :title
217
- field :body
218
- embedded_in :post, :inverse_of => :comments
219
- end
220
-
221
- user = User.create(:name => "Aaron")
222
- post = Post.create(:title => "Test", :body => "Post", :modifier => user)
223
- comment = post.comments.build(:title => "test", :body => "comment", :modifier => user)
224
- post.save
225
- post.history_tracks.count # should be 1
226
-
227
- comment.respond_to?(:history_tracks) # should be false
228
-
229
- track = post.history_tracks.first
230
- track.original # {}
231
- track.modified # { "title" => "Test", "body" => "Post", "comments" => [{ "_id" => "575fa9e667d827e5ed00000d", "title" => "test", "body" => "comment" }], ... }
232
- ```
233
-
234
- **Whitelist the tracked attributes of embedded relations**
235
-
236
- If you don't want to track all the attributes of embedded relations in parent audit history, you can whitelist the attributes as below:
237
-
238
- ```ruby
239
- class Book
240
- include Mongoid::Document
241
- ...
242
- embeds_many :pages
243
- track_history :on => { :pages => [:title, :content] }
244
- end
245
-
246
- class Page
247
- include Mongoid::Document
248
- ...
249
- field :number
250
- field :title
251
- field :subtitle
252
- field :content
253
- embedded_in :book
254
- end
255
- ```
256
-
257
- It will now track only `_id` (Mandatory), `title` and `content` attributes for `pages` relation.
258
-
259
- **Retrieving the list of tracked static and dynamic fields**
260
-
261
- ```ruby
262
- class Book
263
- ...
264
- field :title
265
- field :author
266
- field :price
267
- track_history :on => [:title, :price]
268
- end
269
-
270
- Book.tracked_fields #=> ["title", "price"]
271
- Book.tracked_field?(:title) #=> true
272
- Book.tracked_field?(:author) #=> false
273
- ```
274
-
275
- **Retrieving the list of tracked relations**
276
-
277
- ```ruby
278
- class Book
279
- ...
280
- track_history :on => [:pages]
281
- end
282
-
283
- Book.tracked_relation?(:pages) #=> true
284
- Book.tracked_embeds_many #=> ["pages"]
285
- Book.tracked_embeds_many?(:pages) #=> true
286
- ```
287
-
288
- **Skip soft-deleted embedded objects with nested tracking**
289
-
290
- Default paranoia field is `deleted_at`. You can use custom field for each class as below:
291
-
292
- ```ruby
293
- class Book
294
- include Mongoid::Document
295
- include Mongoid::History::Trackable
296
- embeds_many :pages
297
- track_history on: :pages
298
- end
299
-
300
- class Page
301
- include Mongoid::Document
302
- include Mongoid::History::Trackable
303
- ...
304
- embedded_in :book
305
- history_settings paranoia_field: :removed_at
306
- end
307
- ```
308
-
309
- This will skip the `page` documents with `removed_at` set to a non-blank value from nested tracking
310
-
311
- **Formatting fields**
312
-
313
- You can opt to use a proc or string interpolation to alter attributes being stored on a history record.
314
-
315
- ```ruby
316
- class Post
317
- include Mongoid::Document
318
- include Mongoid::History::Trackable
319
-
320
- field :title
321
- track_history on: :title,
322
- format: { title: ->(t){ t[0..3] } }
323
- ```
324
-
325
- This also works for fields on an embedded relations.
326
-
327
- ```ruby
328
- class Book
329
- include Mongoid::Document
330
- include Mongoid::History::Trackable
331
-
332
- embeds_many :pages
333
- track_history on: :pages,
334
- format: { pages: { number: 'pg. %d' } }
335
- end
336
-
337
- class Page
338
- include Mongoid::Document
339
- include Mongoid::History::Trackable
340
-
341
- field :number, type: Integer
342
- embedded_in :book
343
- end
344
- ```
345
-
346
- **Displaying history trackers as an audit trail**
347
-
348
- In your Controller:
349
-
350
- ```ruby
351
- # Fetch history trackers
352
- @trackers = HistoryTracker.limit(25)
353
-
354
- # get change set for the first tracker
355
- @changes = @trackers.first.tracked_changes
356
- #=> {field: {to: val1, from: val2}}
357
-
358
- # get edit set for the first tracker
359
- @edits = @trackers.first.tracked_edits
360
- #=> { add: {field: val},
361
- # remove: {field: val},
362
- # modify: { to: val1, from: val2 },
363
- # array: { add: [val2], remove: [val1] } }
364
- ```
365
-
366
- In your View, you might do something like (example in HAML format):
367
-
368
- ```haml
369
- %ul.changes
370
- - (@edits[:add]||[]).each do |k,v|
371
- %li.remove Added field #{k} value #{v}
372
-
373
- - (@edits[:modify]||[]).each do |k,v|
374
- %li.modify Changed field #{k} from #{v[:from]} to #{v[:to]}
375
-
376
- - (@edits[:array]||[]).each do |k,v|
377
- %li.modify
378
- - if v[:remove].nil?
379
- Changed field #{k} by adding #{v[:add]}
380
- - elsif v[:add].nil?
381
- Changed field #{k} by removing #{v[:remove]}
382
- - else
383
- Changed field #{k} by adding #{v[:add]} and removing #{v[:remove]}
384
-
385
- - (@edits[:remove]||[]).each do |k,v|
386
- %li.remove Removed field #{k} (was previously #{v})
387
- ```
388
-
389
- **Adding Userstamp on History Trackers**
390
-
391
- To track the User in the application who created the HistoryTracker, please add the
392
- [Mongoid::Userstamp gem](https://github.com/tbpro/mongoid_userstamp) to your HistoryTracker class.
393
- This will add a field called `created_by` and an accessor `creator` to the model (you can rename these via gem config).
394
-
395
- ```
396
- class MyHistoryTracker
397
- include Mongoid::History::Tracker
398
- include Mongoid::Userstamp
399
- end
400
- ```
401
-
402
- *Migrating Userstamp from Previous Versions*
403
-
404
- 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
405
- instructions above then run the following command:
406
-
407
- ```
408
- MyHistoryTracker.all.rename(modifier_id: :created_by)
409
- ```
410
-
411
- **Setting Modifier Class Name**
412
-
413
- 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:
414
-
415
- The following examples set the modifier class name using a Rails initializer:
416
-
417
- If your app uses a class ``Author``:
418
-
419
- ```ruby
420
- # config/initializers/mongoid-history.rb
421
- # initializer for mongoid-history
422
-
423
- Mongoid::History.modifier_class_name = 'Author'
424
- ```
425
-
426
- Or perhaps you are namespacing to a module:
427
-
428
- ```ruby
429
- Mongoid::History.modifier_class_name = 'CMS::Author'
430
- ```
431
-
432
- **Using an alternate changes method**
433
-
434
- Sometimes you may wish to provide an alternate method for determining which changes should be tracked. For example, if you are using embedded documents
435
- and nested attributes, you may wish to write your own changes method that includes changes from the embedded documents.
436
-
437
- Mongoid::History provides an option named `:changes_method` which allows you to do this. It defaults to `:changes`, which is the standard changes method.
438
-
439
- 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]`
440
-
441
- Example:
442
-
443
- ```ruby
444
- class Foo
445
- include Mongoid::Document
446
- include Mongoid::History::Trackable
447
-
448
- attr_accessor :ip
449
-
450
- track_history on: [:ip], changes_method: :my_changes
451
-
452
- def my_changes
453
- unless ip.nil?
454
- changes.merge(ip: [nil, ip])
455
- else
456
- changes
457
- end
458
- end
459
- end
460
- ```
461
-
462
- Example with embedded & nested attributes:
463
-
464
- ```ruby
465
- class Foo
466
- include Mongoid::Document
467
- include Mongoid::Timestamps
468
- include Mongoid::History::Trackable
469
-
470
- field :bar
471
- embeds_one :baz
472
- accepts_nested_attributes_for :baz
473
-
474
- # use changes_with_baz to include baz's changes in this document's
475
- # history.
476
- track_history on: fields.keys + [:baz], changes_method: :changes_with_baz
477
-
478
- def changes_with_baz
479
- if baz.changed?
480
- changes.merge(baz: summarized_changes(baz))
481
- else
482
- changes
483
- end
484
- end
485
-
486
- private
487
- # This method takes the changes from an embedded doc and formats them
488
- # in a summarized way, similar to how the embedded doc appears in the
489
- # parent document's attributes
490
- def summarized_changes obj
491
- obj.changes.keys.map do |field|
492
- next unless obj.respond_to?("#{field}_change")
493
- [ { field => obj.send("#{field}_change")[0] },
494
- { field => obj.send("#{field}_change")[1] } ]
495
- end.compact.transpose.map do |fields|
496
- fields.inject({}) {|map,f| map.merge(f)}
497
- end
498
- end
499
- end
500
-
501
- class Baz
502
- include Mongoid::Document
503
- include Mongoid::Timestamps
504
-
505
- embedded_in :foo
506
- field :value
507
- end
508
- ```
509
-
510
- For more examples, check out [spec/integration/integration_spec.rb](spec/integration/integration_spec.rb).
511
-
512
-
513
- **Thread Safety**
514
-
515
- Mongoid::History stores the tracking enable/disable flag in `Thread.current`.
516
- If the [RequestStore](https://github.com/steveklabnik/request_store) gem is installed, Mongoid::History
517
- will automatically store variables in the `RequestStore.store` instead. RequestStore is recommended
518
- for threaded web servers like Thin or Puma.
519
-
520
-
521
- Contributing to mongoid-history
522
- -------------------------------
523
-
524
- You're encouraged to contribute to this library. See [CONTRIBUTING](CONTRIBUTING.md) for details.
525
-
526
- Copyright
527
- ---------
528
-
529
- Copyright (c) 2011-2016 Aaron Qian and Contributors.
530
-
531
- 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.