{"id":13484323,"url":"https://github.com/mongoid/mongoid-history","last_synced_at":"2025-04-05T15:09:58.527Z","repository":{"id":46995197,"uuid":"1429138","full_name":"mongoid/mongoid-history","owner":"mongoid","description":"Multi-user non-linear history tracking, auditing, undo, redo for mongoid.","archived":false,"fork":false,"pushed_at":"2023-10-31T19:41:23.000Z","size":599,"stargazers_count":390,"open_issues_count":48,"forks_count":128,"subscribers_count":15,"default_branch":"master","last_synced_at":"2024-05-22T20:21:31.757Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/mongoid-history","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mongoid.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2011-03-02T03:35:17.000Z","updated_at":"2024-06-18T13:48:27.697Z","dependencies_parsed_at":"2024-06-18T13:58:25.104Z","dependency_job_id":null,"html_url":"https://github.com/mongoid/mongoid-history","commit_stats":null,"previous_names":["aq1018/mongoid-history"],"tags_count":45,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mongoid%2Fmongoid-history","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mongoid%2Fmongoid-history/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mongoid%2Fmongoid-history/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mongoid%2Fmongoid-history/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mongoid","download_url":"https://codeload.github.com/mongoid/mongoid-history/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247353749,"owners_count":20925329,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:22.544Z","updated_at":"2025-04-05T15:09:58.507Z","avatar_url":"https://github.com/mongoid.png","language":"Ruby","readme":"# Mongoid History\n\n[![Gem Version](https://badge.fury.io/rb/mongoid-history.svg)](http://badge.fury.io/rb/mongoid-history)\n[![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)\n[![Code Climate](https://codeclimate.com/github/mongoid/mongoid-history.svg)](https://codeclimate.com/github/mongoid/mongoid-history)\n\nMongoid 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.\n\nThis 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).\n\n### Version Support\n\nMongoid History supports the following dependency versions:\n\n* Ruby 2.3+\n* Mongoid 3.1+\n* Recent JRuby versions\n\nEarlier Ruby versions may work but are untested.\n\n## Install\n\n```ruby\ngem 'mongoid-history'\n```\n\n## Usage\n\n### Create a history tracker\n\nCreate 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`\n\n```ruby\n# app/models/history_tracker.rb\nclass HistoryTracker\n  include Mongoid::History::Tracker\nend\n```\n\n### Set default tracker class name (optional)\n\nMongoid::History will use the first loaded class to include Mongoid::History::Tracker as the\ndefault history tracker. If you are using multiple Tracker classes, you should set a global\ndefault in a Rails initializer:\n\n```ruby\n# config/initializers/mongoid_history.rb\n# initializer for mongoid-history\n# assuming HistoryTracker is your tracker class\nMongoid::History.tracker_class_name = :history_tracker\n```\n\n### Create trackable classes and objects\n\n```ruby\nclass Post\n  include Mongoid::Document\n  include Mongoid::Timestamps\n\n  # history tracking all Post documents\n  # note: tracking will not work until #track_history is invoked\n  include Mongoid::History::Trackable\n\n  field           :title\n  field           :body\n  field           :rating\n  embeds_many     :comments\n\n  # telling Mongoid::History how you want to track changes\n  # dynamic fields will be tracked automatically (for MongoId 4.0+ you should include Mongoid::Attributes::Dynamic to your model)\n  track_history   :on =\u003e [:title, :body],       # track title and body fields only, default is :all\n                  :modifier_field =\u003e :modifier, # adds \"belongs_to :modifier\" to track who made the change, default is :modifier, set to nil to not create modifier_field\n                  :modifier_field_inverse_of =\u003e :nil, # adds an \":inverse_of\" option to the \"belongs_to :modifier\" relation, default is not set\n                  :modifier_field_optional =\u003e true, # marks the modifier relationship as optional (requires Mongoid 6 or higher)\n                  :version_field =\u003e :version,   # adds \"field :version, :type =\u003e Integer\" to track current version, default is :version\n                  :track_create  =\u003e true,       # track document creation, default is true\n                  :track_update  =\u003e true,       # track document updates, default is true\n                  :track_destroy =\u003e true,       # track document destruction, default is true\n                  :track_blank_changes =\u003e false # track changes from blank? to blank?, default is false\nend\n\nclass Comment\n  include Mongoid::Document\n  include Mongoid::Timestamps\n\n  # declare that we want to track comments\n  include Mongoid::History::Trackable\n\n  field             :title\n  field             :body\n  embedded_in       :post, :inverse_of =\u003e :comments\n\n  # track title and body for all comments, scope it to post (the parent)\n  # also track creation and destruction\n  track_history     :on =\u003e [:title, :body], :scope =\u003e :post, :track_create =\u003e true, :track_destroy =\u003e true\n\n  # For embedded polymorphic relations, specify an array of model names or its polymorphic name\n  # e.g. :scope =\u003e [:post, :image, :video]\n  #      :scope =\u003e :commentable\n\nend\n\n# the modifier class\nclass User\n  include Mongoid::Document\n  include Mongoid::Timestamps\n\n  field             :name\nend\n\nuser = User.create(:name =\u003e \"Aaron\")\npost = Post.create(:title =\u003e \"Test\", :body =\u003e \"Post\", :modifier =\u003e user)\ncomment = post.comments.create(:title =\u003e \"test\", :body =\u003e \"comment\", :modifier =\u003e user)\ncomment.history_tracks.count # should be 1\n\ncomment.update_attributes(:title =\u003e \"Test 2\")\ncomment.history_tracks.count # should be 2\n\ntrack = comment.history_tracks.last\n\ntrack.undo! user # comment title should be \"Test\"\n\ntrack.redo! user # comment title should be \"Test 2\"\n\n# undo comment to version 1 without save\ncomment.undo nil, from: 1, to: comment.version\n\n# undo last change\ncomment.undo! user\n\n# undo versions 1 - 4\ncomment.undo! user, :from =\u003e 4, :to =\u003e 1\n\n# undo last 3 versions\ncomment.undo! user, :last =\u003e 3\n\n# redo versions 1 - 4\ncomment.redo! user, :from =\u003e 1, :to =\u003e 4\n\n# redo last 3 versions\ncomment.redo! user, :last =\u003e 3\n\n# redo version 1\ncomment.redo! user, 1\n\n# delete post\npost.destroy\n\n# undelete post\npost.undo! user\n\n# disable tracking for comments within a block\nComment.disable_tracking do\n  comment.update_attributes(:title =\u003e \"Test 3\")\nend\n\n# disable tracking for comments by default\nComment.disable_tracking!\n\n# enable tracking for comments within a block\nComment.enable_tracking do\n  comment.update_attributes(:title =\u003e \"Test 3\")\nend\n\n# renable tracking for comments by default\nComment.enable_tracking!\n\n# globally disable all history tracking within a block\nMongoid::History.disable do\n  comment.update_attributes(:title =\u003e \"Test 3\")\n  user.update_attributes(:name =\u003e \"Eddie Van Halen\")\nend\n\n# globally disable all history tracking by default\nMongoid::History.disable!\n\n# globally enable all history tracking within a block\nMongoid::History.enable do\n  comment.update_attributes(:title =\u003e \"Test 3\")\n  user.update_attributes(:name =\u003e \"Eddie Van Halen\")\nend\n\n# globally renable all history tracking by default\nMongoid::History.enable!\n```\n\nYou may want to track changes on all fields.\n\n```ruby\nclass Post\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  field           :title\n  field           :body\n  field           :rating\n\n  track_history   :on =\u003e [:fields] # all fields will be tracked\nend\n```\n\nYou can also track changes on all embedded relations.\n\n```ruby\nclass Post\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  embeds_many :comments\n  embeds_one  :content\n\n  track_history   :on =\u003e [:embedded_relations] # all embedded relations will be tracked\nend\n```\n\n**Include embedded objects attributes in parent audit**\n\nModify above `Post` and `Comment` classes as below:\n\n```ruby\nclass Post\n  include Mongoid::Document\n  include Mongoid::Timestamps\n  include Mongoid::History::Trackable\n\n  field           :title\n  field           :body\n  field           :rating\n  embeds_many     :comments\n\n  track_history   :on =\u003e [:title, :body, :comments],\n                  :modifier_field =\u003e :modifier,\n                  :modifier_field_inverse_of =\u003e :nil,\n                  :version_field =\u003e :version,\n                  :track_create   =\u003e  true,     # track create on Post\n                  :track_update   =\u003e  true,\n                  :track_destroy  =\u003e  false\nend\n\nclass Comment\n  include Mongoid::Document\n  include Mongoid::Timestamps\n\n  field             :title\n  field             :body\n  embedded_in       :post, :inverse_of =\u003e :comments\nend\n\nuser = User.create(:name =\u003e \"Aaron\")\npost = Post.create(:title =\u003e \"Test\", :body =\u003e \"Post\", :modifier =\u003e user)\ncomment = post.comments.build(:title =\u003e \"test\", :body =\u003e \"comment\", :modifier =\u003e user)\npost.save\npost.history_tracks.count # should be 1\n\ncomment.respond_to?(:history_tracks) # should be false\n\ntrack = post.history_tracks.first\ntrack.original # {}\ntrack.modified # { \"title\" =\u003e \"Test\", \"body\" =\u003e \"Post\", \"comments\" =\u003e [{ \"_id\" =\u003e \"575fa9e667d827e5ed00000d\", \"title\" =\u003e \"test\", \"body\" =\u003e \"comment\" }], ... }\n```\n\n### Whitelist the tracked attributes of embedded relations\n\nIf you don't want to track all the attributes of embedded relations in parent audit history, you can whitelist the attributes as below:\n\n```ruby\nclass Book\n  include Mongoid::Document\n  ...\n  embeds_many :pages\n  track_history :on =\u003e { :pages =\u003e [:title, :content] }\nend\n\nclass Page\n  include Mongoid::Document\n  ...\n  field :number\n  field :title\n  field :subtitle\n  field :content\n  embedded_in :book\nend\n```\n\nIt will now track only `_id` (Mandatory), `title` and `content` attributes for `pages` relation.\n\n### Track all blank changes\n\nNormally changes where both the original and modified values respond with `true` to `blank?` (for example `nil` to `false`) aren't tracked. However, there may be cases where it's important to track such changes, for example when a field isn't present (so appears to be `nil`) then is set to `false`. To track such changes, set the `track_blank_changes` option to `true` (it defaults to `false`) when turning on history tracking:\n\n```ruby\nclass Book\n  include Mongoid::Document\n  ...\n  field :summary\n  track_history # Use default of false for track_blank_changes\nend\n\n# summary change not tracked if summary hasn't been set (or has been set to something that responds true to blank?)\nBook.find(id).update_attributes(:summary =\u003e '')\n\nclass Chapter\n  include Mongoid::Document\n  ...\n  field :title\n  track_history :track_blank_changes =\u003e true\nend\n\n# title change tracked even if title hasn't been set\nChapter.find(id).update_attributes(:title =\u003e '')\n```\n\n### Retrieving the list of tracked static and dynamic fields\n\n```ruby\nclass Book\n  ...\n  field             :title\n  field             :author\n  field             :price\n  track_history     :on =\u003e [:title, :price]\nend\n\nBook.tracked_fields           #=\u003e [\"title\", \"price\"]\nBook.tracked_field?(:title)   #=\u003e true\nBook.tracked_field?(:author)  #=\u003e false\n```\n\n### Retrieving the list of tracked relations\n\n```ruby\nclass Book\n  ...\n  track_history :on =\u003e [:pages]\nend\n\nBook.tracked_relation?(:pages)    #=\u003e true\nBook.tracked_embeds_many          #=\u003e [\"pages\"]\nBook.tracked_embeds_many?(:pages) #=\u003e true\n```\n\n### Skip soft-deleted embedded objects with nested tracking\n\nDefault paranoia field is `deleted_at`. You can use custom field for each class as below:\n\n```ruby\nclass Book\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n  embeds_many :pages\n  track_history on: :pages\nend\n\nclass Page\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n  ...\n  embedded_in :book\n  history_settings paranoia_field: :removed_at\nend\n```\n\nThis will skip the `page` documents with `removed_at` set to a non-blank value from nested tracking\n\n### Formatting fields\n\nYou can opt to use a proc or string interpolation to alter attributes being stored on a history record.\n\n```ruby\nclass Post\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  field           :title\n  track_history   on: :title,\n                  format: { title: -\u003e(t){ t[0..3] } }\n```\n\nThis also works for fields on an embedded relations.\n\n```ruby\nclass Book\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  embeds_many :pages\n  track_history on: :pages,\n                format: { pages: { number: 'pg. %d' } }\nend\n\nclass Page\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  field :number, type: Integer\n  embedded_in :book\nend\n```\n\n### Displaying history trackers as an audit trail\n\nIn your Controller:\n\n```ruby\n# Fetch history trackers\n@trackers = HistoryTracker.limit(25)\n\n# get change set for the first tracker\n@changes = @trackers.first.tracked_changes\n  #=\u003e {field: {to: val1, from: val2}}\n\n# get edit set for the first tracker\n@edits = @trackers.first.tracked_edits\n  #=\u003e { add: {field: val},\n  #     remove: {field: val},\n  #     modify: { to: val1, from: val2 },\n  #     array: { add: [val2], remove: [val1] } }\n```\n\nIn your View, you might do something like (example in HAML format):\n\n```haml\n%ul.changes\n  - (@edits[:add]||[]).each do |k,v|\n    %li.remove Added field #{k} value #{v}\n\n  - (@edits[:modify]||[]).each do |k,v|\n    %li.modify Changed field #{k} from #{v[:from]} to #{v[:to]}\n\n  - (@edits[:array]||[]).each do |k,v|\n    %li.modify\n      - if v[:remove].nil?\n        Changed field #{k} by adding #{v[:add]}\n      - elsif v[:add].nil?\n        Changed field #{k} by removing #{v[:remove]}\n      - else\n        Changed field #{k} by adding #{v[:add]} and removing #{v[:remove]}\n\n  - (@edits[:remove]||[]).each do |k,v|\n    %li.remove Removed field #{k} (was previously #{v})\n```\n\n### Adding Userstamp on History Trackers\n\nTo track the User in the application who created the HistoryTracker, add the\n[Mongoid::Userstamp gem](https://github.com/tbpro/mongoid_userstamp) to your HistoryTracker class.\nThis will add a field called `created_by` and an accessor `creator` to the model (you can rename these via gem config).\n\n```\nclass MyHistoryTracker\n  include Mongoid::History::Tracker\n  include Mongoid::Userstamp\nend\n```\n\n### Setting Modifier Class Name\n\nIf 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:\n\nThe following examples set the modifier class name using a Rails initializer:\n\nIf your app uses a class ``Author``:\n\n```ruby\n# config/initializers/mongoid-history.rb\n# initializer for mongoid-history\n\nMongoid::History.modifier_class_name = 'Author'\n```\n\nOr perhaps you are namespacing to a module:\n\n```ruby\nMongoid::History.modifier_class_name = 'CMS::Author'\n```\n\n### Conditional :if and :unless options\n\nThe `track_history` method supports `:if` and `:unless` options which will skip generating\nthe history tracker unless they are satisfied. These options can take either a method\n`Symbol` or a `Proc`. They behave identical to how `:if` and `:unless` behave in Rails model callbacks.\n\n```ruby\n  track_history on: [:ip],\n                if: :should_i_track_history?,\n                unless: -\u003e(obj){ obj.method_to_skip_history }\n```\n\n### Using an alternate changes method\n\nSometimes you may wish to provide an alternate method for determining which changes should be tracked.  For example, if you are using embedded documents\nand nested attributes, you may wish to write your own changes method that includes changes from the embedded documents.\n\nMongoid::History provides an option named `:changes_method` which allows you to do this.  It defaults to `:changes`, which is the standard changes method.\n\nNote: 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]`\n\nExample:\n\n```ruby\nclass Foo\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  attr_accessor :ip\n\n  track_history on: [:ip], changes_method: :my_changes\n\n  def my_changes\n    unless ip.nil?\n      changes.merge(ip: [nil, ip])\n    else\n      changes\n    end\n  end\nend\n```\n\nExample with embedded \u0026 nested attributes:\n\n```ruby\nclass Foo\n  include Mongoid::Document\n  include Mongoid::Timestamps\n  include Mongoid::History::Trackable\n\n  field      :bar\n  embeds_one :baz\n  accepts_nested_attributes_for :baz\n\n  # use changes_with_baz to include baz's changes in this document's\n  # history.\n  track_history   on: fields.keys + [:baz], changes_method: :changes_with_baz\n\n  def changes_with_baz\n    if baz.changed?\n      changes.merge(baz: summarized_changes(baz))\n    else\n      changes\n    end\n  end\n\n  private\n  # This method takes the changes from an embedded doc and formats them\n  # in a summarized way, similar to how the embedded doc appears in the\n  # parent document's attributes\n  def summarized_changes obj\n    obj.changes.keys.map do |field|\n      next unless obj.respond_to?(\"#{field}_change\")\n      [ { field =\u003e obj.send(\"#{field}_change\")[0] },\n        { field =\u003e obj.send(\"#{field}_change\")[1] } ]\n    end.compact.transpose.map do |fields|\n      fields.inject({}) {|map,f| map.merge(f)}\n    end\n  end\nend\n\nclass Baz\n  include Mongoid::Document\n  include Mongoid::Timestamps\n\n  embedded_in :foo\n  field :value\nend\n```\n\nFor more examples, check out [spec/integration/integration_spec.rb](spec/integration/integration_spec.rb).\n\n### Multiple Trackers\n\nYou can have different trackers for different classes like so.\n\n``` ruby\nclass First\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  field :text, type: String\n  track_history on: [:text],\n                tracker_class_name: :first_history_tracker\nend\n\nclass Second\n  include Mongoid::Document\n  include Mongoid::History::Trackable\n\n  field :text, type: String\n  track_history on: [:text],\n                tracker_class_name: :second_history_tracker\nend\n\nclass FirstHistoryTracker\n  include Mongoid::History::Tracker\nend\n\nclass SecondHistoryTracker\n  include Mongoid::History::Tracker\nend\n```\n\nNote that if you are using a tracker for an embedded object that is different\nfrom the parent's tracker, redos and undos will not work. You have to use the\nsame tracker for these to work across embedded relationships.\n\nIf you are using multiple trackers and the `tracker_class_name` parameter is\nnot specified, Mongoid::History will use the default tracker configured in the\ninitializer file or whatever the first tracker was loaded.\n\n### Dependent Restrict Associations\n\nWhen `dependent: :restrict` is used on an association, a call to `destroy` on\nthe model will raise `Mongoid::Errors::DeleteRestriction` when the dependency\nis violated. Just be aware that this gem will create a history track document\nbefore the `destroy` call and then remove if an error is raised. This applies\nto all persistence calls: create, update and destroy.\n\nSee [spec/integration/validation_failure_spec.rb](spec/integration/validation_failure_spec.rb)\nfor examples.\n\n### Thread Safety\n\nMongoid::History stores the tracking enable/disable flag in `Thread.current`.\nIf the [RequestStore](https://github.com/steveklabnik/request_store) gem is installed, Mongoid::History\nwill automatically store variables in the `RequestStore.store` instead. RequestStore is recommended\nfor threaded web servers like Thin or Puma.\n\n\n## Contributing\n\nYou're encouraged to contribute to Mongoid History. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## Copyright\n\nCopyright (c) 2011-2024 Aaron Qian and Contributors.\n\nMIT License. See [LICENSE.txt](LICENSE.txt) for further details.\n","funding_links":[],"categories":["ORM/ODM Extensions"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmongoid%2Fmongoid-history","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmongoid%2Fmongoid-history","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmongoid%2Fmongoid-history/lists"}