├── .coveralls.yml ├── .document ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dangerfile ├── Gemfile ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── Rakefile ├── UPGRADING.md ├── lib ├── mongoid-history.rb └── mongoid │ ├── history.rb │ └── history │ ├── attributes │ ├── base.rb │ ├── create.rb │ ├── destroy.rb │ └── update.rb │ ├── options.rb │ ├── trackable.rb │ ├── tracker.rb │ └── version.rb ├── mongoid-history.gemspec ├── perf ├── benchmark_modified_attributes_for_create.rb └── gc_suite.rb └── spec ├── integration ├── embedded_in_polymorphic_spec.rb ├── integration_spec.rb ├── multi_relation_spec.rb ├── multiple_trackers_spec.rb ├── nested_embedded_documents_spec.rb ├── nested_embedded_documents_tracked_in_parent_spec.rb ├── nested_embedded_polymorphic_documents_spec.rb ├── subclasses_spec.rb ├── track_history_order_spec.rb └── validation_failure_spec.rb ├── spec_helper.rb ├── support ├── error_helpers.rb ├── mongoid.rb └── mongoid_history.rb └── unit ├── attributes ├── base_spec.rb ├── create_spec.rb ├── destroy_spec.rb └── update_spec.rb ├── callback_options_spec.rb ├── embedded_methods_spec.rb ├── history_spec.rb ├── my_instance_methods_spec.rb ├── options_spec.rb ├── singleton_methods_spec.rb ├── store ├── default_store_spec.rb └── request_store_spec.rb ├── trackable_spec.rb └── tracker_spec.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI RSpec Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: >- 8 | ${{ matrix.ruby }} 9 | env: 10 | CI: true 11 | TESTOPTS: -v 12 | runs-on: ubuntu-latest 13 | continue-on-error: ${{ matrix.experimental }} 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | ruby: [2.5, 2.6, 2.7, 3.0, jruby] # truffleruby 18 | mongoid: [7] 19 | experimental: [false] 20 | include: 21 | - ruby: 2.3 22 | mongoid: 3 23 | experimental: false 24 | - ruby: 2.4 25 | mongoid: 4 26 | experimental: false 27 | - ruby: 2.5 28 | mongoid: 5 29 | experimental: false 30 | - ruby: 2.6 31 | mongoid: 6 32 | experimental: false 33 | - ruby: 2.7 34 | mongoid: 7.0 35 | experimental: false 36 | - ruby: 2.7 37 | mongoid: 7.1 38 | experimental: false 39 | - ruby: 2.7 40 | mongoid: 7.2 41 | experimental: false 42 | - ruby: 2.7 43 | mongoid: 7.3 44 | experimental: false 45 | - ruby: head 46 | mongoid: 7 47 | experimental: true 48 | - ruby: jruby-head 49 | mongoid: 7 50 | experimental: true 51 | # - ruby: truffleruby-head 52 | # mongoid: 7 53 | # experimental: true 54 | steps: 55 | - name: repo checkout 56 | uses: actions/checkout@v2 57 | - name: start mongodb 58 | uses: supercharge/mongodb-github-action@1.6.0 59 | with: 60 | mongodb-version: 4.4 61 | mongodb-replica-set: rs0 62 | - name: load ruby 63 | uses: ruby/setup-ruby@v1 64 | with: 65 | ruby-version: ${{ matrix.ruby }} 66 | bundler: 2 67 | - name: bundle install 68 | run: bundle install --jobs 4 --retry 3 69 | - name: test 70 | timeout-minutes: 10 71 | run: bundle exec rake spec 72 | continue-on-error: ${{ matrix.experimental }} 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | 3 | .rvmrc 4 | 5 | # rcov generated 6 | coverage 7 | 8 | # rdoc generated 9 | rdoc 10 | 11 | # yard generated 12 | doc 13 | .yardoc 14 | 15 | # bundler 16 | .bundle 17 | 18 | # jeweler generated 19 | pkg 20 | 21 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 22 | # 23 | # * Create a file at ~/.gitignore 24 | # * Include files you want ignored 25 | # * Run: git config --global core.excludesfile ~/.gitignore 26 | # 27 | # After doing this, these files will be ignored in all your git projects, 28 | # saving you from having to 'pollute' every project you touch with them 29 | # 30 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 31 | # 32 | # For MacOS: 33 | # 34 | #.DS_Store 35 | # 36 | # For TextMate 37 | #*.tmproj 38 | #tmtags 39 | # 40 | # For emacs: 41 | #*~ 42 | #\#* 43 | #.\#* 44 | # 45 | # For vim: 46 | #*.swp 47 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=documentation 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - vendor/**/* 4 | - bin/**/* 5 | 6 | inherit_from: .rubocop_todo.yml 7 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2020-04-10 17:25:32 -0500 using RuboCop version 0.48.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 7 10 | # Configuration parameters: Include. 11 | # Include: **/Gemfile, **/gems.rb 12 | Bundler/DuplicatedGem: 13 | Exclude: 14 | - 'Gemfile' 15 | 16 | # Offense count: 3 17 | Lint/HandleExceptions: 18 | Exclude: 19 | - 'spec/unit/trackable_spec.rb' 20 | 21 | # Offense count: 3 22 | Lint/ParenthesesAsGroupedExpression: 23 | Exclude: 24 | - 'spec/integration/integration_spec.rb' 25 | - 'spec/integration/nested_embedded_polymorphic_documents_spec.rb' 26 | 27 | # Offense count: 22 28 | Metrics/AbcSize: 29 | Max: 52 30 | 31 | # Offense count: 122 32 | # Configuration parameters: CountComments, ExcludedMethods. 33 | Metrics/BlockLength: 34 | Max: 900 35 | 36 | # Offense count: 1 37 | # Configuration parameters: CountComments. 38 | Metrics/ClassLength: 39 | Max: 121 40 | 41 | # Offense count: 6 42 | Metrics/CyclomaticComplexity: 43 | Max: 13 44 | 45 | # Offense count: 412 46 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 47 | # URISchemes: http, https 48 | Metrics/LineLength: 49 | Max: 688 50 | 51 | # Offense count: 17 52 | # Configuration parameters: CountComments. 53 | Metrics/MethodLength: 54 | Max: 23 55 | 56 | # Offense count: 2 57 | # Configuration parameters: CountComments. 58 | Metrics/ModuleLength: 59 | Max: 200 60 | 61 | # Offense count: 6 62 | Metrics/PerceivedComplexity: 63 | Max: 15 64 | 65 | # Offense count: 15 66 | Style/Documentation: 67 | Exclude: 68 | - 'spec/**/*' 69 | - 'test/**/*' 70 | - 'lib/mongoid/history.rb' 71 | - 'lib/mongoid/history/attributes/base.rb' 72 | - 'lib/mongoid/history/attributes/create.rb' 73 | - 'lib/mongoid/history/attributes/destroy.rb' 74 | - 'lib/mongoid/history/attributes/update.rb' 75 | - 'lib/mongoid/history/options.rb' 76 | - 'lib/mongoid/history/trackable.rb' 77 | - 'lib/mongoid/history/tracker.rb' 78 | - 'perf/benchmark_modified_attributes_for_create.rb' 79 | - 'perf/gc_suite.rb' 80 | 81 | # Offense count: 3 82 | # Cop supports --auto-correct. 83 | Style/EachWithObject: 84 | Exclude: 85 | - 'lib/mongoid/history/trackable.rb' 86 | - 'lib/mongoid/history/tracker.rb' 87 | 88 | # Offense count: 2 89 | # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. 90 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 91 | Style/FileName: 92 | Exclude: 93 | - 'Dangerfile' 94 | - 'lib/mongoid-history.rb' 95 | 96 | # Offense count: 1 97 | Style/MultilineBlockChain: 98 | Exclude: 99 | - 'lib/mongoid/history/tracker.rb' 100 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.9.0 (Next) 2 | 3 | * [#257](https://github.com/mongoid/mongoid-history/pull/257): Add track_blank_changes option - [@BrianLMatthews](https://github.com/BrianLMatthews). 4 | * Your contribution here. 5 | 6 | ### 0.8.5 (2021/09/18) 7 | 8 | * [#250](https://github.com/mongoid/mongoid-history/pull/250): Migrate to Github actions - [@johnnyshields](https://github.com/johnnyshields). 9 | * [#249](https://github.com/mongoid/mongoid-history/pull/249): Don't update version on embedded documents if the document itself is being destroyed - [@getaroom](https://github.com/getaroom). 10 | * [#248](https://github.com/mongoid/mongoid-history/pull/248): Don't update version on embedded documents if an ancestor is being destroyed in the same operation - [@getaroom](https://github.com/getaroom). 11 | 12 | ### 0.8.4 (2021/09/18) 13 | 14 | * Not released. 15 | 16 | ### 0.8.3 (2020/06/17) 17 | 18 | * [#236](https://github.com/mongoid/mongoid-history/pull/236): Fix Ruby 2.7 keyword argument warnings - [@vasilysn](https://github.com/vasilysn). 19 | * [#237](https://github.com/mongoid/mongoid-history/pull/237): Fix tracking subclasses with additional fields - [@getaroom](https://github.com/getaroom). 20 | * [#239](https://github.com/mongoid/mongoid-history/pull/239): Optimize `modified_attributes_for_create` 6-7x - [@getaroom](https://github.com/getaroom). 21 | * [#240](https://github.com/mongoid/mongoid-history/pull/240): `Mongoid::History.disable` and `disable_tracking` now restore the original state - [@getaroom](https://github.com/getaroom). 22 | * [#240](https://github.com/mongoid/mongoid-history/pull/240): Added `Mongoid::History.enable`, `Mongoid::History.enable!`, `Mongoid::History.disable!`, `enable_tracking`, `enable_tracking!`, and `disable_tracking!` - [@getaroom](https://github.com/getaroom). 23 | 24 | ### 0.8.2 (2019/12/02) 25 | 26 | * [#233](https://github.com/mongoid/mongoid-history/pull/233): Bug fix-Track ALL embedded relations when used with fields and filtered attributes on embedded objects - [@jagdeepsingh](https://github.com/jagdeepsingh). 27 | * [#232](https://github.com/mongoid/mongoid-history/pull/232): Bug/187 track changes from embedded documents (not deeply nested) - [@Startouf](https://github.com/Startouf). 28 | * [#227](https://github.com/mongoid/mongoid-history/pull/227): Store options in inheritable class attributes - [@jnfeinstein](https://github.com/jnfeinstein). 29 | * [#229](https://github.com/mongoid/mongoid-history/pull/229), [#225](https://github.com/mongoid/mongoid-history/pull/225): Fixed inheritance of `history_trackable_options` - [@jnfeinstein](https://github.com/jnfeinstein). 30 | 31 | ### 0.8.1 (2018/06/28) 32 | 33 | * [#221](https://github.com/mongoid/mongoid-history/pull/221): Mongoid 7 support - [@dblock](https://github.com/dblock). 34 | 35 | ### 0.8.0 (2018/01/16) 36 | 37 | * [#180](https://github.com/mongoid/mongoid-history/pull/180): Removed deprecation notice - [@sivagollapalli](https://github.com/sivagollapalli). 38 | * [#208](https://github.com/mongoid/mongoid-history/pull/208): Fix: history tracks fields declared after `track_history` - [@mikwat](https://github.com/mikwat). 39 | * [#210](https://github.com/mongoid/mongoid-history/pull/210): Do not track unmodified embedded relations - [@jagdeepsingh](https://github.com/jagdeepsingh). 40 | * [#205](https://github.com/mongoid/mongoid-history/pull/205): Allow modifier field to be optional - [@yads](https://github.com/yads). 41 | * [#211](https://github.com/mongoid/mongoid-history/pull/211): Enable tracking create/destroy by default - [@jagdeepsingh](https://github.com/jagdeepsingh). 42 | * [#212](https://github.com/mongoid/mongoid-history/pull/212): `track_history` method support for `:if` and `:unless` options - [@jagdeepsingh](https://github.com/jagdeepsingh). 43 | 44 | ### 0.7.0 (2017/11/14) 45 | 46 | * [#202](https://github.com/mongoid/mongoid-history/pull/202): Do not create tracker on persistence error - [@mikwat](https://github.com/mikwat). 47 | * [#196](https://github.com/mongoid/mongoid-history/pull/196): Fix bug causing history tracks to get mixed up between multiple trackers when using multiple trackers - [@ojbucao](https://github.com/ojbucao). 48 | 49 | ### 0.6.1 (2017/01/04) 50 | 51 | * [#182](https://github.com/mongoid/mongoid-history/pull/182): No-op on repeated calls to destroy - [@msaffitz](https://github.com/msaffitz). 52 | * [#170](https://github.com/mongoid/mongoid-history/pull/170): Parent repo is now [mongoid/mongoid-history](https://github.com/mongoid/mongoid-history) - [@dblock](https://github.com/dblock). 53 | * [#171](https://github.com/mongoid/mongoid-history/pull/171): Add field formatting - [@jnfeinstein](https://github.com/jnfeinstein). 54 | * [#172](https://github.com/mongoid/mongoid-history/pull/172): Add config helper to track all embedded relations - [@jnfeinstein](https://github.com/jnfeinstein). 55 | * [#173](https://github.com/mongoid/mongoid-history/pull/173): Compatible with mongoid 6 - [@sivagollapalli](https://github.com/sivagollapalli). 56 | 57 | ### 0.6.0 (2016/09/13) 58 | 59 | * [#2](https://github.com/dblock/mongoid-history/pull/2): Forked into the [mongoid](https://github.com/mongoid) organization - [@dblock](https://github.com/dblock). 60 | * [#1](https://github.com/dblock/mongoid-history/pull/1): Added Danger, PR linter - [@dblock](https://github.com/dblock). 61 | * [#166](https://github.com/mongoid/mongoid-history/pull/166): Hash fields should default to an empty Hash - [@johnnyshields](https://github.com/johnnyshields). 62 | * [#162](https://github.com/mongoid/mongoid-history/pull/162): Do not consider embedded relations as dynamic fields - [@JagdeepSingh](https://github.com/JagdeepSingh). 63 | * [#144](https://github.com/mongoid/mongoid-history/pull/158): Can modify history tracker insertion on object creation - [@sivagollapalli](https://github.com/sivagollapalli). 64 | * [#155](https://github.com/mongoid/mongoid-history/pull/155): Add support to whitelist the attributes for tracked embeds_one and embeds_many relations - [@JagdeepSingh](https://github.com/JagdeepSingh). 65 | * [#154](https://github.com/mongoid/mongoid-history/pull/154): Prevent soft-deleted embedded documents from tracking - [@JagdeepSingh](https://github.com/JagdeepSingh). 66 | * [#151](https://github.com/mongoid/mongoid-history/pull/151): Added ability to customize tracker class for each trackable; multiple trackers across the app are now possible - [@JagdeepSingh](https://github.com/JagdeepSingh). 67 | * [#151](https://github.com/mongoid/mongoid-history/pull/151): Added automatic support for `request_store` gem as drop-in replacement for `Thread.current` - [@JagdeepSingh](https://github.com/JagdeepSingh). 68 | * [#150](https://github.com/mongoid/mongoid-history/pull/150): Added support for keeping embedded objects audit history in parent itself - [@JagdeepSingh](https://github.com/JagdeepSingh). 69 | 70 | ### 0.5.0 (2015/09/18) 71 | 72 | * [#143](https://github.com/mongoid/mongoid-history/pull/143): Added support for Mongoid 5 - [@dblock](https://github.com/dblock). 73 | * [#133](https://github.com/mongoid/mongoid-history/pull/133): Added dynamic attributes tracking (Mongoid::Attributes::Dynamic) - [@minisai](https://github.com/minisai). 74 | * [#142](https://github.com/mongoid/mongoid-history/pull/142): Allow non-database fields to be specified in conjunction with a custom changes method - [@kayakyakr](https://github.com/kayakyakr). 75 | 76 | ### 0.4.7 (2015/04/06) 77 | 78 | * [#124](https://github.com/mongoid/mongoid-history/pull/124): You can require both `mongoid-history` and `mongoid/history` - [@dblock](https://github.com/dblock). 79 | 80 | ### 0.4.5 (2015/02/09) 81 | 82 | * [#131](https://github.com/mongoid/mongoid-history/pull/131): Added `undo` method, that helps to get specific version of an object without saving changes - [@alexkravets](https://github.com/alexkravets). 83 | * [#127](https://github.com/mongoid/mongoid-history/pull/127): Fixed gem naming per [rubygems](http://guides.rubygems.org/name-your-gem/) specs, now you can `require 'mongoid/history'` - [@nofxx](https://github.com/nofxx). 84 | * [#129](https://github.com/mongoid/mongoid-history/pull/129): Support multiple levels of embedded polimorphic documents - [@BrunoChauvet](https://github.com/BrunoChauvet). 85 | * [#123](https://github.com/mongoid/mongoid-history/pull/123): Used a method compatible with mongoid-observers to determinine the version of Mongoid - [@zeitnot](https://github.com/zeitnot). 86 | 87 | ### 0.4.4 (2014/7/29) 88 | 89 | * [#111](https://github.com/mongoid/mongoid-history/pull/111): Fixed compatibility of `undo!` and `redo!` methods with Rails 3.x - [@mrjlynch](https://github.com/mrjlynch). 90 | 91 | ### 0.4.3 (2014/07/10) 92 | 93 | * [#110](https://github.com/mongoid/mongoid-history/pull/110): Fixed scope reference on history tracks criteria - [@adbeelitamar](https://github.com/adbeelitamar). 94 | 95 | ### 0.4.2 (2014/07/01) 96 | 97 | * [#106](https://github.com/mongoid/mongoid-history/pull/106): Added support for polymorphic relationship `scope` - [@adbeelitamar](https://github.com/adbeelitamar). 98 | * [#106](https://github.com/mongoid/mongoid-history/pull/106): Enabled specifying an array of relationships in `scope` - [@adbeelitamar](https://github.com/adbeelitamar). 99 | * [#83](https://github.com/mongoid/mongoid-history/pull/83): Added support for Mongoid 4.x, which removed `attr_accessible` in favor of protected attributes - [@dblock](https://github.com/dblock). 100 | * [#103](https://github.com/mongoid/mongoid-history/pull/103): Fixed compatibility with models using `default_scope` - [@mrjlynch](https://github.com/mrjlynch). 101 | 102 | ### 0.4.1 (2014/01/11) 103 | 104 | * Fixed compatibility with Mongoid 4.x - [@dblock](https://github.com/dblock). 105 | * `Mongoid::History::Sweeper` has been removed, in accorance with Mongoid 4.x (see [#3108](https://github.com/mongoid/mongoid/issues/3108)) and Rails 4.x observer deprecation - [@dblock](https://github.com/dblock). 106 | * Default modifier parameter to `nil` in `undo!` and `redo!` - [@dblock](https://github.com/dblock). 107 | * Fixed `undo!` and `redo!` for mass-assignment protected attributes - [@mati0090](https://github.com/mati0090). 108 | * Implemented Rubocop, Ruby style linter - [@dblock](https://github.com/dblock). 109 | * Remove unneeded coma from README - [@matsprea](https://github.com/matsprea). 110 | * Replace Jeweler with Gem-Release - [@johnnyshields](https://github.com/johnnyshields). 111 | * Track version as a Ruby file - [@johnnyshields](https://github.com/johnnyshields). 112 | 113 | ### 0.4.0 (2013/06/12) 114 | 115 | * Added `Mongoid::History.disable` and `Mongoid::History.enabled?` methods for global tracking disablement - [@johnnyshields](https://github.com/johnnyshields). 116 | * Added `:changes_method` that optionally overrides which method to call to collect changes - [@joelnordel](https://github.com/joelnordell). 117 | * The `:destroy` action now stores trackers in the format `original=value, modified=nil` (previously it was the reverse) - [@johnnyshields](https://github.com/johnnyshields). 118 | * Support for polymorphic embedded classes - [@tstepp](https://github.com/tstepp). 119 | * Support for Mongoid field aliases, e.g. `field :n, as: :name` - [@johnnyshields](https://github.com/johnnyshields). 120 | * Support for Mongoid embedded aliases, e.g. `embeds_many :comments, store_as: :coms` - [@johnnyshields](https://github.com/johnnyshields). 121 | * Added `#tracked_changes` and `#tracked_edits` methods to `Tracker` class for nicer change summaries - [@johnnyshields](https://github.com/johnnyshields) and [@tstepp](https://github.com/tstepp). 122 | * Refactored and exposed `#trackable_parent_class` in `Tracker`, which returns the class of the trackable regardless of whether the trackable itself has been destroyed - [@johnnyshields](https://github.com/johnnyshields). 123 | * Added class-level `#tracked_field?` and `#tracked_fields` methods; refactor logic to determine whether a field is tracked - [@johnnyshields](https://github.com/johnnyshields). 124 | * Fixed bug in Trackable#track_update where `return` condition at beginning of method caused a short-circuit where memoization would not be cleared properly - [@johnnyshields](https://github.com/johnnyshields). 125 | * Tests: Added spec for nested embedded documents - [@matekb](https://github.com/matekb). 126 | * Tests: Test run time cut in half (~2.5s versus ~5s) by using `#let` helper and removing class initialization before each test - [@johnnyshields](https://github.com/johnnyshields). 127 | * Tests: Remove `database_cleaner` gem in favor of `Mongoid.purge!` - [@johnnyshields](https://github.com/johnnyshields). 128 | * Tests: Remove dependency on non-committed file `mongoid.yml` and hardcode collection to `mongoid_history_test` - [@johnnyshields](https://github.com/johnnyshields). 129 | 130 | ### 0.3.3 (2013/04/01) 131 | 132 | * [#42](https://github.com/mongoid/mongoid-history/issues/42): Fix: corrected creation of association chain when using nested embedded documents - [@matekb](https://github.com/matekb). 133 | * [#56](https://github.com/mongoid/mongoid-history/issues/56): Fix: now possible to undo setting (creating) attributes that was previously unset - [@matekb](https://github.com/matekb). 134 | * [#49](https://github.com/mongoid/mongoid-history/issues/49): Fix: now correctly undo/redo localized fields - [@edejong](https://github.com/edejong). 135 | 136 | 137 | ### 0.3.2 (2013/01/24) 138 | 139 | * [#54](https://github.com/mongoid/mongoid-history/pull/54): Used an index instead of the `$elemMatch` selector in `history_tracks` - [@vecio](https://github.com/vecio). 140 | * [#11](https://github.com/mongoid/mongoid-history/issues/11): Added `:modifier_field_inverse_of` on `track_history` that defines the `:inverse_of` relationship of the modifier class - [@matekb](https://github.com/matekb), [@dblock](https://github.com/dblock). 141 | 142 | ### 0.3.1 (2012/11/16) 143 | 144 | * [#45](https://github.com/mongoid/mongoid-history/pull/45): Fix: intermittent hash ordering issue with `history_tracks` - [@getaroom](https://github.com/getaroom). 145 | * [#50](https://github.com/mongoid/mongoid-history/pull/50): Fix: tracking of array changes, undo and redo of field changes on non-embedded objects - [@dblock](https://github.com/dblock). 146 | 147 | ### 0.3.0 (2012/08/21) 148 | 149 | * [#41](https://github.com/mongoid/mongoid-history/pull/41): Mongoid 3.x support - [@zambot](https://github.com/zambot). 150 | 151 | ### 0.2.4 (2012/08/21) 152 | 153 | * [#38](https://github.com/mongoid/mongoid-history/pull/38): Fix: allow sub-models to be tracked by using `collection_name` as the scope - [@acant](https://github.com/acant). 154 | * [#35](https://github.com/mongoid/mongoid-history/pull/35): Fix: sweeper references record of change, not the record changed - [@dblock](https://github.com/dblock). 155 | 156 | ### 0.2.3 (2012/04/20) 157 | 158 | * [#23](https://github.com/mongoid/mongoid-history/pull/34): Updated `Trackable::association_hash` to write through parent - [@tcopple](https://github.com/tcopple). 159 | * Fix: `Trackable::association_hash` nil meta value call - [@tcopple](https://github.com/tcopple). 160 | * [#27](https://github.com/mongoid/mongoid-history/pull/27): Added support for re-creation of destroyed embedded documents - [@erlingwl](https://github.com/erlingwl). 161 | 162 | ### 0.1.7 (2011/12/09) 163 | 164 | * Fix: tracking `false` values - [@gottfrois](https://github.com/gottfrois). 165 | * Used a mongoid observer and controller `around_filter` to pick up modifying user from controller - [@bensymonds](https://github.com/bensymonds). 166 | * More flexible dependency on mongoid - [@sarcilav](https://github.com/sarcilav). 167 | * Fix: tracking broken in a multithreaded environment - [@dblock](https://github.com/dblock). 168 | 169 | ### 0.1.0 (2011/05/13) 170 | 171 | * Added support for `destroy` - [@dblock](https://github.com/dblock). 172 | * Added undo and redo - [@aq1018](https://github.com/aq1018). 173 | * Added support for temporarily disabling history tracking - [@aq1018](https://github.com/aq1018). 174 | * Record modifier for undo and redo actions - [@aq1018](https://github.com/aq1018). 175 | 176 | ### 0.0.1 (2011/03/04) 177 | 178 | * Intial public release - [@aq1018](https://github.com/aq1018). 179 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Mongoid-History 2 | 3 | Mongoid-History is work of [many of contributors](https://github.com/mongoid/mongoid-history/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/mongoid/mongoid-history/pulls), [propose features, ask questions and discuss issues](https://github.com/mongoid/mongoid-history/issues). 4 | 5 | ### Fork the Project 6 | 7 | Fork the [project on Github](https://github.com/mongoid/mongoid-history) and check out your copy. 8 | 9 | ``` 10 | git clone https://github.com/contributor/mongoid-history.git 11 | cd mongoid-history 12 | git remote add upstream https://github.com/mongoid/mongoid-history.git 13 | ``` 14 | 15 | ### Create a Topic Branch 16 | 17 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. 18 | 19 | ``` 20 | git checkout master 21 | git pull upstream master 22 | git checkout -b my-feature-branch 23 | ``` 24 | 25 | ### Bundle Install and Test 26 | 27 | Ensure that you can build the project and run tests. 28 | 29 | ``` 30 | bundle install 31 | bundle exec rake 32 | ``` 33 | 34 | ### Write Tests 35 | 36 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/mongoid-history](spec/mongoid-history). 37 | 38 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. 39 | 40 | ### Write Code 41 | 42 | Implement your feature or bug fix. 43 | 44 | Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted. 45 | 46 | Make sure that `bundle exec rake` completes without errors. 47 | 48 | ### Write Documentation 49 | 50 | Document any external behavior in the [README](README.md). 51 | 52 | ### Update Changelog 53 | 54 | Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account. 55 | 56 | ### Commit Changes 57 | 58 | Make sure git knows your name and email address: 59 | 60 | ``` 61 | git config --global user.name "Your Name" 62 | git config --global user.email "contributor@example.com" 63 | ``` 64 | 65 | Writing good commit logs is important. A commit log should describe what changed and why. 66 | 67 | ``` 68 | git add ... 69 | git commit 70 | ``` 71 | 72 | ### Push 73 | 74 | ``` 75 | git push origin my-feature-branch 76 | ``` 77 | 78 | ### Make a Pull Request 79 | 80 | Go to https://github.com/contributor/mongoid-history and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. 81 | 82 | ### Rebase 83 | 84 | If you've been working on a change for a while, rebase with upstream/master. 85 | 86 | ``` 87 | git fetch upstream 88 | git rebase upstream/master 89 | git push origin my-feature-branch -f 90 | ``` 91 | 92 | ### Update CHANGELOG Again 93 | 94 | Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. 95 | 96 | ``` 97 | * [#123](https://github.com/mongoid/mongoid-history/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). 98 | ``` 99 | 100 | Amend your previous commit and force push the changes. 101 | 102 | ``` 103 | git commit --amend 104 | git push origin my-feature-branch -f 105 | ``` 106 | 107 | ### Check on Your Pull Request 108 | 109 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. 110 | 111 | ### Be Patient 112 | 113 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! 114 | 115 | ### Thank You 116 | 117 | Please do know that we really appreciate and value your time and work. We love you, really. 118 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | danger.import_dangerfile(gem: 'mongoid-danger') 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | case version = ENV['MONGOID_VERSION'] || '~> 7.0' 6 | when 'HEAD' 7 | gem 'mongoid', github: 'mongodb/mongoid' 8 | when '7' 9 | gem 'mongoid', '~> 7.3' 10 | when '7.3' 11 | gem 'mongoid', '~> 7.3.0' 12 | when '7.2' 13 | gem 'mongoid', '~> 7.2.0' 14 | when '7.1' 15 | gem 'mongoid', '~> 7.1.0' 16 | when '7.0' 17 | gem 'mongoid', '~> 7.0.0' 18 | when '6' 19 | gem 'mongoid', '~> 6.0' 20 | when '5' 21 | gem 'mongoid', '~> 5.0' 22 | gem 'mongoid-observers', '~> 0.2' 23 | when '4' 24 | gem 'mongoid', '~> 4.0' 25 | gem 'mongoid-observers', '~> 0.2' 26 | when '3' 27 | gem 'mongoid', '~> 3.1' 28 | else 29 | gem 'mongoid', version 30 | end 31 | 32 | gem 'mongoid-compatibility' 33 | 34 | group :development, :test do 35 | gem 'bundler' 36 | gem 'pry' 37 | gem 'rake' 38 | end 39 | 40 | group :test do 41 | gem 'benchmark-ips', require: false 42 | gem 'coveralls' 43 | gem 'gem-release' 44 | gem 'mongoid-danger', '~> 0.1.0', require: false 45 | gem 'request_store' 46 | gem 'rspec', '~> 3.1' 47 | gem 'rubocop', '~> 0.49.0' 48 | gem 'term-ansicolor', '~> 1.3.0' 49 | gem 'yard' 50 | end 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2020 Aaron Qian & Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | :track_blank_changes => false # track changes from blank? to blank?, default is false 80 | end 81 | 82 | class Comment 83 | include Mongoid::Document 84 | include Mongoid::Timestamps 85 | 86 | # declare that we want to track comments 87 | include Mongoid::History::Trackable 88 | 89 | field :title 90 | field :body 91 | embedded_in :post, :inverse_of => :comments 92 | 93 | # track title and body for all comments, scope it to post (the parent) 94 | # also track creation and destruction 95 | track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true 96 | 97 | # For embedded polymorphic relations, specify an array of model names or its polymorphic name 98 | # e.g. :scope => [:post, :image, :video] 99 | # :scope => :commentable 100 | 101 | end 102 | 103 | # the modifier class 104 | class User 105 | include Mongoid::Document 106 | include Mongoid::Timestamps 107 | 108 | field :name 109 | end 110 | 111 | user = User.create(:name => "Aaron") 112 | post = Post.create(:title => "Test", :body => "Post", :modifier => user) 113 | comment = post.comments.create(:title => "test", :body => "comment", :modifier => user) 114 | comment.history_tracks.count # should be 1 115 | 116 | comment.update_attributes(:title => "Test 2") 117 | comment.history_tracks.count # should be 2 118 | 119 | track = comment.history_tracks.last 120 | 121 | track.undo! user # comment title should be "Test" 122 | 123 | track.redo! user # comment title should be "Test 2" 124 | 125 | # undo comment to version 1 without save 126 | comment.undo nil, from: 1, to: comment.version 127 | 128 | # undo last change 129 | comment.undo! user 130 | 131 | # undo versions 1 - 4 132 | comment.undo! user, :from => 4, :to => 1 133 | 134 | # undo last 3 versions 135 | comment.undo! user, :last => 3 136 | 137 | # redo versions 1 - 4 138 | comment.redo! user, :from => 1, :to => 4 139 | 140 | # redo last 3 versions 141 | comment.redo! user, :last => 3 142 | 143 | # redo version 1 144 | comment.redo! user, 1 145 | 146 | # delete post 147 | post.destroy 148 | 149 | # undelete post 150 | post.undo! user 151 | 152 | # disable tracking for comments within a block 153 | Comment.disable_tracking do 154 | comment.update_attributes(:title => "Test 3") 155 | end 156 | 157 | # disable tracking for comments by default 158 | Comment.disable_tracking! 159 | 160 | # enable tracking for comments within a block 161 | Comment.enable_tracking do 162 | comment.update_attributes(:title => "Test 3") 163 | end 164 | 165 | # renable tracking for comments by default 166 | Comment.enable_tracking! 167 | 168 | # globally disable all history tracking within a block 169 | Mongoid::History.disable do 170 | comment.update_attributes(:title => "Test 3") 171 | user.update_attributes(:name => "Eddie Van Halen") 172 | end 173 | 174 | # globally disable all history tracking by default 175 | Mongoid::History.disable! 176 | 177 | # globally enable all history tracking within a block 178 | Mongoid::History.enable do 179 | comment.update_attributes(:title => "Test 3") 180 | user.update_attributes(:name => "Eddie Van Halen") 181 | end 182 | 183 | # globally renable all history tracking by default 184 | Mongoid::History.enable! 185 | ``` 186 | 187 | You may want to track changes on all fields. 188 | 189 | ```ruby 190 | class Post 191 | include Mongoid::Document 192 | include Mongoid::History::Trackable 193 | 194 | field :title 195 | field :body 196 | field :rating 197 | 198 | track_history :on => [:fields] # all fields will be tracked 199 | end 200 | ``` 201 | 202 | You can also track changes on all embedded relations. 203 | 204 | ```ruby 205 | class Post 206 | include Mongoid::Document 207 | include Mongoid::History::Trackable 208 | 209 | embeds_many :comments 210 | embeds_one :content 211 | 212 | track_history :on => [:embedded_relations] # all embedded relations will be tracked 213 | end 214 | ``` 215 | 216 | **Include embedded objects attributes in parent audit** 217 | 218 | Modify above `Post` and `Comment` classes as below: 219 | 220 | ```ruby 221 | class Post 222 | include Mongoid::Document 223 | include Mongoid::Timestamps 224 | include Mongoid::History::Trackable 225 | 226 | field :title 227 | field :body 228 | field :rating 229 | embeds_many :comments 230 | 231 | track_history :on => [:title, :body, :comments], 232 | :modifier_field => :modifier, 233 | :modifier_field_inverse_of => :nil, 234 | :version_field => :version, 235 | :track_create => true, # track create on Post 236 | :track_update => true, 237 | :track_destroy => false 238 | end 239 | 240 | class Comment 241 | include Mongoid::Document 242 | include Mongoid::Timestamps 243 | 244 | field :title 245 | field :body 246 | embedded_in :post, :inverse_of => :comments 247 | end 248 | 249 | user = User.create(:name => "Aaron") 250 | post = Post.create(:title => "Test", :body => "Post", :modifier => user) 251 | comment = post.comments.build(:title => "test", :body => "comment", :modifier => user) 252 | post.save 253 | post.history_tracks.count # should be 1 254 | 255 | comment.respond_to?(:history_tracks) # should be false 256 | 257 | track = post.history_tracks.first 258 | track.original # {} 259 | track.modified # { "title" => "Test", "body" => "Post", "comments" => [{ "_id" => "575fa9e667d827e5ed00000d", "title" => "test", "body" => "comment" }], ... } 260 | ``` 261 | 262 | ### Whitelist the tracked attributes of embedded relations 263 | 264 | If you don't want to track all the attributes of embedded relations in parent audit history, you can whitelist the attributes as below: 265 | 266 | ```ruby 267 | class Book 268 | include Mongoid::Document 269 | ... 270 | embeds_many :pages 271 | track_history :on => { :pages => [:title, :content] } 272 | end 273 | 274 | class Page 275 | include Mongoid::Document 276 | ... 277 | field :number 278 | field :title 279 | field :subtitle 280 | field :content 281 | embedded_in :book 282 | end 283 | ``` 284 | 285 | It will now track only `_id` (Mandatory), `title` and `content` attributes for `pages` relation. 286 | 287 | ### Track all blank changes 288 | 289 | Normally 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: 290 | 291 | ```ruby 292 | class Book 293 | include Mongoid::Document 294 | ... 295 | field :summary 296 | track_history # Use default of false for track_blank_changes 297 | end 298 | 299 | # summary change not tracked if summary hasn't been set (or has been set to something that responds true to blank?) 300 | Book.find(id).update_attributes(:summary => '') 301 | 302 | class Chapter 303 | include Mongoid::Document 304 | ... 305 | field :title 306 | track_history :track_blank_changes => true 307 | end 308 | 309 | # title change tracked even if title hasn't been set 310 | Chapter.find(id).update_attributes(:title => '') 311 | ``` 312 | 313 | ### Retrieving the list of tracked static and dynamic fields 314 | 315 | ```ruby 316 | class Book 317 | ... 318 | field :title 319 | field :author 320 | field :price 321 | track_history :on => [:title, :price] 322 | end 323 | 324 | Book.tracked_fields #=> ["title", "price"] 325 | Book.tracked_field?(:title) #=> true 326 | Book.tracked_field?(:author) #=> false 327 | ``` 328 | 329 | ### Retrieving the list of tracked relations 330 | 331 | ```ruby 332 | class Book 333 | ... 334 | track_history :on => [:pages] 335 | end 336 | 337 | Book.tracked_relation?(:pages) #=> true 338 | Book.tracked_embeds_many #=> ["pages"] 339 | Book.tracked_embeds_many?(:pages) #=> true 340 | ``` 341 | 342 | ### Skip soft-deleted embedded objects with nested tracking 343 | 344 | Default paranoia field is `deleted_at`. You can use custom field for each class as below: 345 | 346 | ```ruby 347 | class Book 348 | include Mongoid::Document 349 | include Mongoid::History::Trackable 350 | embeds_many :pages 351 | track_history on: :pages 352 | end 353 | 354 | class Page 355 | include Mongoid::Document 356 | include Mongoid::History::Trackable 357 | ... 358 | embedded_in :book 359 | history_settings paranoia_field: :removed_at 360 | end 361 | ``` 362 | 363 | This will skip the `page` documents with `removed_at` set to a non-blank value from nested tracking 364 | 365 | ### Formatting fields 366 | 367 | You can opt to use a proc or string interpolation to alter attributes being stored on a history record. 368 | 369 | ```ruby 370 | class Post 371 | include Mongoid::Document 372 | include Mongoid::History::Trackable 373 | 374 | field :title 375 | track_history on: :title, 376 | format: { title: ->(t){ t[0..3] } } 377 | ``` 378 | 379 | This also works for fields on an embedded relations. 380 | 381 | ```ruby 382 | class Book 383 | include Mongoid::Document 384 | include Mongoid::History::Trackable 385 | 386 | embeds_many :pages 387 | track_history on: :pages, 388 | format: { pages: { number: 'pg. %d' } } 389 | end 390 | 391 | class Page 392 | include Mongoid::Document 393 | include Mongoid::History::Trackable 394 | 395 | field :number, type: Integer 396 | embedded_in :book 397 | end 398 | ``` 399 | 400 | ### Displaying history trackers as an audit trail 401 | 402 | In your Controller: 403 | 404 | ```ruby 405 | # Fetch history trackers 406 | @trackers = HistoryTracker.limit(25) 407 | 408 | # get change set for the first tracker 409 | @changes = @trackers.first.tracked_changes 410 | #=> {field: {to: val1, from: val2}} 411 | 412 | # get edit set for the first tracker 413 | @edits = @trackers.first.tracked_edits 414 | #=> { add: {field: val}, 415 | # remove: {field: val}, 416 | # modify: { to: val1, from: val2 }, 417 | # array: { add: [val2], remove: [val1] } } 418 | ``` 419 | 420 | In your View, you might do something like (example in HAML format): 421 | 422 | ```haml 423 | %ul.changes 424 | - (@edits[:add]||[]).each do |k,v| 425 | %li.remove Added field #{k} value #{v} 426 | 427 | - (@edits[:modify]||[]).each do |k,v| 428 | %li.modify Changed field #{k} from #{v[:from]} to #{v[:to]} 429 | 430 | - (@edits[:array]||[]).each do |k,v| 431 | %li.modify 432 | - if v[:remove].nil? 433 | Changed field #{k} by adding #{v[:add]} 434 | - elsif v[:add].nil? 435 | Changed field #{k} by removing #{v[:remove]} 436 | - else 437 | Changed field #{k} by adding #{v[:add]} and removing #{v[:remove]} 438 | 439 | - (@edits[:remove]||[]).each do |k,v| 440 | %li.remove Removed field #{k} (was previously #{v}) 441 | ``` 442 | 443 | ### Adding Userstamp on History Trackers 444 | 445 | To track the User in the application who created the HistoryTracker, add the 446 | [Mongoid::Userstamp gem](https://github.com/tbpro/mongoid_userstamp) to your HistoryTracker class. 447 | This will add a field called `created_by` and an accessor `creator` to the model (you can rename these via gem config). 448 | 449 | ``` 450 | class MyHistoryTracker 451 | include Mongoid::History::Tracker 452 | include Mongoid::Userstamp 453 | end 454 | ``` 455 | 456 | ### Setting Modifier Class Name 457 | 458 | 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: 459 | 460 | The following examples set the modifier class name using a Rails initializer: 461 | 462 | If your app uses a class ``Author``: 463 | 464 | ```ruby 465 | # config/initializers/mongoid-history.rb 466 | # initializer for mongoid-history 467 | 468 | Mongoid::History.modifier_class_name = 'Author' 469 | ``` 470 | 471 | Or perhaps you are namespacing to a module: 472 | 473 | ```ruby 474 | Mongoid::History.modifier_class_name = 'CMS::Author' 475 | ``` 476 | 477 | ### Conditional :if and :unless options 478 | 479 | The `track_history` method supports `:if` and `:unless` options which will skip generating 480 | the history tracker unless they are satisfied. These options can take either a method 481 | `Symbol` or a `Proc`. They behave identical to how `:if` and `:unless` behave in Rails model callbacks. 482 | 483 | ```ruby 484 | track_history on: [:ip], 485 | if: :should_i_track_history?, 486 | unless: ->(obj){ obj.method_to_skip_history } 487 | ``` 488 | 489 | ### Using an alternate changes method 490 | 491 | Sometimes you may wish to provide an alternate method for determining which changes should be tracked. For example, if you are using embedded documents 492 | and nested attributes, you may wish to write your own changes method that includes changes from the embedded documents. 493 | 494 | Mongoid::History provides an option named `:changes_method` which allows you to do this. It defaults to `:changes`, which is the standard changes method. 495 | 496 | 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]` 497 | 498 | Example: 499 | 500 | ```ruby 501 | class Foo 502 | include Mongoid::Document 503 | include Mongoid::History::Trackable 504 | 505 | attr_accessor :ip 506 | 507 | track_history on: [:ip], changes_method: :my_changes 508 | 509 | def my_changes 510 | unless ip.nil? 511 | changes.merge(ip: [nil, ip]) 512 | else 513 | changes 514 | end 515 | end 516 | end 517 | ``` 518 | 519 | Example with embedded & nested attributes: 520 | 521 | ```ruby 522 | class Foo 523 | include Mongoid::Document 524 | include Mongoid::Timestamps 525 | include Mongoid::History::Trackable 526 | 527 | field :bar 528 | embeds_one :baz 529 | accepts_nested_attributes_for :baz 530 | 531 | # use changes_with_baz to include baz's changes in this document's 532 | # history. 533 | track_history on: fields.keys + [:baz], changes_method: :changes_with_baz 534 | 535 | def changes_with_baz 536 | if baz.changed? 537 | changes.merge(baz: summarized_changes(baz)) 538 | else 539 | changes 540 | end 541 | end 542 | 543 | private 544 | # This method takes the changes from an embedded doc and formats them 545 | # in a summarized way, similar to how the embedded doc appears in the 546 | # parent document's attributes 547 | def summarized_changes obj 548 | obj.changes.keys.map do |field| 549 | next unless obj.respond_to?("#{field}_change") 550 | [ { field => obj.send("#{field}_change")[0] }, 551 | { field => obj.send("#{field}_change")[1] } ] 552 | end.compact.transpose.map do |fields| 553 | fields.inject({}) {|map,f| map.merge(f)} 554 | end 555 | end 556 | end 557 | 558 | class Baz 559 | include Mongoid::Document 560 | include Mongoid::Timestamps 561 | 562 | embedded_in :foo 563 | field :value 564 | end 565 | ``` 566 | 567 | For more examples, check out [spec/integration/integration_spec.rb](spec/integration/integration_spec.rb). 568 | 569 | ### Multiple Trackers 570 | 571 | You can have different trackers for different classes like so. 572 | 573 | ``` ruby 574 | class First 575 | include Mongoid::Document 576 | include Mongoid::History::Trackable 577 | 578 | field :text, type: String 579 | track_history on: [:text], 580 | tracker_class_name: :first_history_tracker 581 | end 582 | 583 | class Second 584 | include Mongoid::Document 585 | include Mongoid::History::Trackable 586 | 587 | field :text, type: String 588 | track_history on: [:text], 589 | tracker_class_name: :second_history_tracker 590 | end 591 | 592 | class FirstHistoryTracker 593 | include Mongoid::History::Tracker 594 | end 595 | 596 | class SecondHistoryTracker 597 | include Mongoid::History::Tracker 598 | end 599 | ``` 600 | 601 | Note that if you are using a tracker for an embedded object that is different 602 | from the parent's tracker, redos and undos will not work. You have to use the 603 | same tracker for these to work across embedded relationships. 604 | 605 | If you are using multiple trackers and the `tracker_class_name` parameter is 606 | not specified, Mongoid::History will use the default tracker configured in the 607 | initializer file or whatever the first tracker was loaded. 608 | 609 | ### Dependent Restrict Associations 610 | 611 | When `dependent: :restrict` is used on an association, a call to `destroy` on 612 | the model will raise `Mongoid::Errors::DeleteRestriction` when the dependency 613 | is violated. Just be aware that this gem will create a history track document 614 | before the `destroy` call and then remove if an error is raised. This applies 615 | to all persistence calls: create, update and destroy. 616 | 617 | See [spec/integration/validation_failure_spec.rb](spec/integration/validation_failure_spec.rb) 618 | for examples. 619 | 620 | ### Thread Safety 621 | 622 | Mongoid::History stores the tracking enable/disable flag in `Thread.current`. 623 | If the [RequestStore](https://github.com/steveklabnik/request_store) gem is installed, Mongoid::History 624 | will automatically store variables in the `RequestStore.store` instead. RequestStore is recommended 625 | for threaded web servers like Thin or Puma. 626 | 627 | 628 | ## Contributing 629 | 630 | You're encouraged to contribute to Mongoid History. See [CONTRIBUTING.md](CONTRIBUTING.md) for details. 631 | 632 | ## Copyright 633 | 634 | Copyright (c) 2011-2024 Aaron Qian and Contributors. 635 | 636 | MIT License. See [LICENSE.txt](LICENSE.txt) for further details. 637 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Mongoid::History 2 | 3 | There are no particular rules about when to release Mongoid History. 4 | Release bug fixes frequently, features not so frequently and breaking API changes rarely. 5 | 6 | ### Release Procedure 7 | 8 | Run tests, check that all tests succeed locally. 9 | 10 | ``` 11 | bundle install 12 | bundle exec rake 13 | ``` 14 | 15 | Check that the last build succeeded in [Travis CI](https://travis-ci.org/mongoid/mongoid-history) 16 | for all supported platforms. 17 | 18 | Increment the version, modify [lib/mongoid/history/version.rb](lib/mongoid/history/version.rb). 19 | Mongoid History versions should follow [Semantic Versioning (SemVer)](https://semver.org/). 20 | 21 | Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version. 22 | 23 | ``` 24 | ### 0.4.0 (2014-01-27) 25 | ``` 26 | 27 | Remove the line with "Your contribution here.", since there will be no more contributions to this release. 28 | 29 | Commit your changes. 30 | 31 | ``` 32 | git add CHANGELOG.md lib/mongoid/history/version.rb 33 | git commit -m "Preparing for release, 0.4.0." 34 | git push origin master 35 | ``` 36 | 37 | Release. 38 | 39 | ``` 40 | $ rake release 41 | 42 | mongoid-history 0.4.0 built to pkg/mongoid-history-0.4.0.gem. 43 | Tagged v0.4.0. 44 | Pushed git commits and tags. 45 | Pushed mongoid-history 0.4.0 to rubygems.org. 46 | ``` 47 | 48 | ### Prepare for the Next Version 49 | 50 | Add the next release to [CHANGELOG.md](CHANGELOG.md). 51 | 52 | ``` 53 | ### 0.4.1 (Next) 54 | 55 | * Your contribution here. 56 | ``` 57 | 58 | Increment the minor version, modify [lib/mongoid/history/version.rb](lib/mongoid/history/version.rb). 59 | 60 | Commit your changes. 61 | 62 | ``` 63 | git add CHANGELOG.md lib/mongoid/history/version.rb 64 | git commit -m "Preparing for next release, 0.4.1." 65 | git push origin master 66 | ``` 67 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | begin 3 | Bundler.setup(:default, :development) 4 | rescue Bundler::BundlerError => e 5 | $stderr.puts e.message 6 | $stderr.puts 'Run `bundle install` to install missing gems' 7 | exit e.status_code 8 | end 9 | 10 | Bundler::GemHelper.install_tasks 11 | 12 | require 'rspec/core' 13 | require 'rspec/core/rake_task' 14 | RSpec::Core::RakeTask.new(:spec) do |spec| 15 | spec.pattern = FileList['spec/**/*_spec.rb'] 16 | end 17 | 18 | require 'rubocop/rake_task' 19 | RuboCop::RakeTask.new(:rubocop) 20 | 21 | task default: %i[rubocop spec] 22 | 23 | require 'yard' 24 | YARD::Rake::YardocTask.new 25 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | ## Upgrading Mongoid History 2 | 3 | ### Upgrading to 0.8.0 4 | 5 | #### History is now tracked on create and destroy by default 6 | 7 | By default, Mongoid History will now track all actions (create, update, and destroy.) 8 | Previously, only update actions were tracked by default. 9 | 10 | To preserve the old behavior, please modify your call to `track_history` as follows: 11 | 12 | ```ruby 13 | track_history ... 14 | track_create: false, 15 | track_destroy: false 16 | ``` 17 | 18 | See [#207](https://github.com/mongoid/mongoid-history/pull/207) for more information. 19 | 20 | ### Upgrading to 0.7.0 21 | 22 | #### Remove history track when create, update or destroy raises an error 23 | 24 | When an error is raised in a call to create, update or destroy a tracked model, any history track 25 | created before the call will now be deleted. In the past this was a problem for associations marked 26 | `dependent: :restrict`. 27 | 28 | See [#202](https://github.com/mongoid/mongoid-history/pull/202) for more information. 29 | 30 | # Main Changes / Upgrading Notes 31 | 32 | See [#189](https://github.com/mongoid/mongoid-history/pull/189) for more information. 33 | 34 | * Currently, the `:all` option behaves identically to `:fields`. Future versions will track all fields and relations of trackable class when using `:all`. 35 | 36 | ### Upgrading to 0.4.1 37 | 38 | #### Migrate Userstamp 39 | 40 | `Mongoid::History` no longer supports the userstamp natively. To track the User in the application who created the HistoryTracker, add the [Mongoid::Userstamp gem](https://github.com/tbpro/mongoid_userstamp) to your HistoryTracker class. 41 | 42 | ```ruby 43 | class MyHistoryTracker 44 | include Mongoid::History::Tracker 45 | include Mongoid::Userstamp 46 | end 47 | ``` 48 | 49 | Rename the field. 50 | 51 | ```ruby 52 | MyHistoryTracker.all.rename(modifier_id: :created_by) 53 | ``` 54 | -------------------------------------------------------------------------------- /lib/mongoid-history.rb: -------------------------------------------------------------------------------- 1 | require 'mongoid/history' 2 | -------------------------------------------------------------------------------- /lib/mongoid/history.rb: -------------------------------------------------------------------------------- 1 | require 'easy_diff' 2 | require 'mongoid/compatibility' 3 | require 'mongoid/history/attributes/base' 4 | require 'mongoid/history/attributes/create' 5 | require 'mongoid/history/attributes/update' 6 | require 'mongoid/history/attributes/destroy' 7 | require 'mongoid/history/options' 8 | require 'mongoid/history/version' 9 | require 'mongoid/history/tracker' 10 | require 'mongoid/history/trackable' 11 | 12 | module Mongoid 13 | module History 14 | GLOBAL_TRACK_HISTORY_FLAG = 'mongoid_history_trackable_enabled'.freeze 15 | 16 | class << self 17 | attr_accessor :tracker_class_name 18 | attr_accessor :trackable_settings 19 | attr_accessor :modifier_class_name 20 | attr_accessor :current_user_method 21 | 22 | def disable 23 | original_flag = store[GLOBAL_TRACK_HISTORY_FLAG] 24 | store[GLOBAL_TRACK_HISTORY_FLAG] = false 25 | yield if block_given? 26 | ensure 27 | store[GLOBAL_TRACK_HISTORY_FLAG] = original_flag if block_given? 28 | end 29 | 30 | def enable 31 | original_flag = store[GLOBAL_TRACK_HISTORY_FLAG] 32 | store[GLOBAL_TRACK_HISTORY_FLAG] = true 33 | yield if block_given? 34 | ensure 35 | store[GLOBAL_TRACK_HISTORY_FLAG] = original_flag if block_given? 36 | end 37 | 38 | alias disable! disable 39 | alias enable! enable 40 | 41 | def enabled? 42 | store[GLOBAL_TRACK_HISTORY_FLAG] != false 43 | end 44 | 45 | def store 46 | defined?(RequestStore) ? RequestStore.store : Thread.current 47 | end 48 | 49 | def default_settings 50 | @default_settings ||= { paranoia_field: 'deleted_at' } 51 | end 52 | 53 | def trackable_class_settings(trackable_class) 54 | trackable_settings[trackable_class.name.to_sym] || default_settings 55 | end 56 | 57 | def reset! 58 | Mongoid::History.modifier_class_name = 'User' 59 | Mongoid::History.trackable_settings = {} 60 | Mongoid::History.current_user_method ||= :current_user 61 | 62 | Mongoid.models.each do |model| 63 | next unless model.included_modules.include? Mongoid::History::Trackable 64 | 65 | model.singleton_class.class_eval do 66 | # Inverse of class_attribute 67 | %i[mongoid_history_options 68 | mongoid_history_options= 69 | mongoid_history_options?].each { |m| remove_possible_method(m) } 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | 77 | Mongoid::History.reset! 78 | -------------------------------------------------------------------------------- /lib/mongoid/history/attributes/base.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | module Attributes 4 | class Base 5 | attr_reader :trackable 6 | 7 | def initialize(trackable) 8 | @trackable = trackable 9 | end 10 | 11 | private 12 | 13 | def trackable_class 14 | @trackable_class ||= trackable.class 15 | end 16 | 17 | def aliased_fields 18 | @aliased_fields ||= trackable_class.aliased_fields 19 | end 20 | 21 | def changes_method 22 | trackable_class.history_trackable_options[:changes_method] 23 | end 24 | 25 | def changes 26 | trackable.send(changes_method) 27 | end 28 | 29 | def format_field(field, value) 30 | format_value(value, trackable_class.field_format(field)) 31 | end 32 | 33 | def format_embeds_one_relation(rel, obj) 34 | rel = trackable_class.database_field_name(rel) 35 | relation_class = trackable_class.relation_class_of(rel) 36 | permitted_attrs = trackable_class.tracked_embeds_one_attributes(rel) 37 | formats = trackable_class.field_format(rel) 38 | format_relation(relation_class, obj, permitted_attrs, formats) 39 | end 40 | 41 | def format_embeds_many_relation(rel, obj) 42 | rel = trackable_class.database_field_name(rel) 43 | relation_class = trackable_class.relation_class_of(rel) 44 | permitted_attrs = trackable_class.tracked_embeds_many_attributes(rel) 45 | formats = trackable_class.field_format(rel) 46 | format_relation(relation_class, obj, permitted_attrs, formats) 47 | end 48 | 49 | def format_relation(relation_class, obj, permitted_attrs, formats) 50 | obj.inject({}) do |m, field_value| 51 | field = relation_class.database_field_name(field_value.first) 52 | next m unless permitted_attrs.include?(field) 53 | 54 | value = field_value.last 55 | value = format_value(field_value.last, formats[field]) if formats.class == Hash 56 | m.merge(field => value) 57 | end 58 | end 59 | 60 | def format_value(value, format) 61 | if format.class == String 62 | format % value 63 | elsif format.respond_to?(:call) 64 | format.call(value) 65 | else 66 | value 67 | end 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/mongoid/history/attributes/create.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | module Attributes 4 | class Create < ::Mongoid::History::Attributes::Base 5 | def attributes 6 | @attributes = {} 7 | changes.each do |k, v| 8 | next unless trackable_class.tracked_field?(k, :create) 9 | @attributes[k] = format_field(k, v) 10 | end 11 | insert_embeds_one_changes 12 | insert_embeds_many_changes 13 | @attributes 14 | end 15 | 16 | private 17 | 18 | def insert_embeds_one_changes 19 | trackable_class.tracked_embeds_one.each do |rel| 20 | rel_class = trackable_class.relation_class_of(rel) 21 | paranoia_field = Mongoid::History.trackable_class_settings(rel_class)[:paranoia_field] 22 | paranoia_field = rel_class.aliased_fields.key(paranoia_field) || paranoia_field 23 | rel = aliased_fields.key(rel) || rel 24 | obj = trackable.send(rel) 25 | next if !obj || (obj.respond_to?(paranoia_field) && obj.public_send(paranoia_field).present?) 26 | @attributes[rel] = [nil, format_embeds_one_relation(rel, obj.attributes)] 27 | end 28 | end 29 | 30 | def insert_embeds_many_changes 31 | trackable_class.tracked_embeds_many.each do |rel| 32 | rel_class = trackable_class.relation_class_of(rel) 33 | paranoia_field = Mongoid::History.trackable_class_settings(rel_class)[:paranoia_field] 34 | paranoia_field = rel_class.aliased_fields.key(paranoia_field) || paranoia_field 35 | rel = aliased_fields.key(rel) || rel 36 | @attributes[rel] = [nil, 37 | trackable.send(rel) 38 | .reject { |obj| obj.respond_to?(paranoia_field) && obj.public_send(paranoia_field).present? } 39 | .map { |obj| format_embeds_many_relation(rel, obj.attributes) }] 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mongoid/history/attributes/destroy.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | module Attributes 4 | class Destroy < ::Mongoid::History::Attributes::Base 5 | def attributes 6 | @attributes = {} 7 | trackable.attributes.each { |k, v| @attributes[k] = [format_field(k, v), nil] if trackable_class.tracked_field?(k, :destroy) } 8 | insert_embeds_one_changes 9 | insert_embeds_many_changes 10 | @attributes 11 | end 12 | 13 | private 14 | 15 | def insert_embeds_one_changes 16 | trackable_class.tracked_embeds_one 17 | .map { |rel| aliased_fields.key(rel) || rel } 18 | .each do |rel| 19 | obj = trackable.send(rel) 20 | @attributes[rel] = [format_embeds_one_relation(rel, obj.attributes), nil] if obj 21 | end 22 | end 23 | 24 | def insert_embeds_many_changes 25 | trackable_class.tracked_embeds_many 26 | .map { |rel| aliased_fields.key(rel) || rel } 27 | .each do |rel| 28 | @attributes[rel] = [trackable.send(rel).map { |obj| format_embeds_many_relation(rel, obj.attributes) }, nil] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mongoid/history/attributes/update.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | module Attributes 4 | class Update < ::Mongoid::History::Attributes::Base 5 | # @example when both an attribute `foo` and a child's attribute `nested_bar.baz` are changed 6 | # 7 | # { 8 | # 'foo' => ['foo_before_changes', 'foo_after_changes'] 9 | # 'nested_bar.baz' => ['nested_bar_baz_before_changes', 'nested_bar_baz_after_changes'] 10 | # } 11 | # } 12 | # 13 | # @return [Hash>] Hash of changes 14 | def attributes 15 | changes_from_parent.deep_merge(changes_from_children) 16 | end 17 | 18 | private 19 | 20 | def changes_from_parent 21 | track_blank_changes = trackable_class.history_trackable_options[:track_blank_changes] 22 | parent_changes = {} 23 | changes.each do |k, v| 24 | change_value = begin 25 | if trackable_class.tracked_embeds_one?(k) 26 | embeds_one_changes_from_parent(k, v) 27 | elsif trackable_class.tracked_embeds_many?(k) 28 | embeds_many_changes_from_parent(k, v) 29 | elsif trackable_class.tracked?(k, :update) 30 | { k => format_field(k, v) } unless !track_blank_changes && v.all?(&:blank?) 31 | end 32 | end 33 | parent_changes.merge!(change_value) if change_value.present? 34 | end 35 | parent_changes 36 | end 37 | 38 | def changes_from_children 39 | embeds_one_changes_from_embedded_documents 40 | end 41 | 42 | # Retrieve the list of changes applied directly to the nested documents 43 | # 44 | # @example when a child's name is changed from "todd" to "mario" 45 | # 46 | # child = Child.new(name: 'todd') 47 | # Parent.create(child: child) 48 | # child.name = "Mario" 49 | # 50 | # embeds_one_changes_from_embedded_documents # when called from "Parent" 51 | # # => { "child.name"=>["todd", "mario"] } 52 | # 53 | # @return [Hash] changes of embeds_ones from embedded documents 54 | def embeds_one_changes_from_embedded_documents 55 | embedded_doc_changes = {} 56 | trackable_class.tracked_embeds_one.each do |rel| 57 | rel_class = trackable_class.relation_class_of(rel) 58 | paranoia_field = Mongoid::History.trackable_class_settings(rel_class)[:paranoia_field] 59 | paranoia_field = rel_class.aliased_fields.key(paranoia_field) || paranoia_field 60 | rel = aliased_fields.key(rel) || rel 61 | obj = trackable.send(rel) 62 | next if !obj || (obj.respond_to?(paranoia_field) && obj.public_send(paranoia_field).present?) 63 | 64 | obj.changes.each do |k, v| 65 | embedded_doc_changes["#{rel}.#{k}"] = [v.first, v.last] 66 | end 67 | end 68 | embedded_doc_changes 69 | end 70 | 71 | # @param [String] relation 72 | # @param [String] value 73 | # 74 | # @return [Hash>] 75 | def embeds_one_changes_from_parent(relation, value) 76 | relation = trackable_class.database_field_name(relation) 77 | relation_class = trackable_class.relation_class_of(relation) 78 | paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field] 79 | original_value = value[0][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[0]) 80 | modified_value = value[1][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[1]) 81 | return if original_value == modified_value 82 | 83 | { relation => [original_value, modified_value] } 84 | end 85 | 86 | # @param [String] relation 87 | # @param [String] value 88 | # 89 | # @return [Hash>] 90 | def embeds_many_changes_from_parent(relation, value) 91 | relation = trackable_class.database_field_name(relation) 92 | relation_class = trackable_class.relation_class_of(relation) 93 | paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field] 94 | original_value = value[0].reject { |rel| rel[paranoia_field].present? } 95 | .map { |v_attrs| format_embeds_many_relation(relation, v_attrs) } 96 | modified_value = value[1].reject { |rel| rel[paranoia_field].present? } 97 | .map { |v_attrs| format_embeds_many_relation(relation, v_attrs) } 98 | return if original_value == modified_value 99 | 100 | { relation => [original_value, modified_value] } 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/mongoid/history/options.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | class Options 4 | attr_reader :trackable, :options 5 | 6 | def initialize(trackable, opts = {}) 7 | @trackable = trackable 8 | @options = default_options.merge(opts) 9 | end 10 | 11 | def scope 12 | trackable.collection_name.to_s.singularize.to_sym 13 | end 14 | 15 | def prepared 16 | return @prepared if @prepared 17 | @prepared = options.dup 18 | prepare_skipped_fields 19 | prepare_formatted_fields 20 | parse_tracked_fields_and_relations 21 | @prepared 22 | end 23 | 24 | private 25 | 26 | def default_options 27 | { on: :all, 28 | except: %i[created_at updated_at], 29 | tracker_class_name: nil, 30 | modifier_field: :modifier, 31 | version_field: :version, 32 | changes_method: :changes, 33 | scope: scope, 34 | track_create: true, 35 | track_update: true, 36 | track_destroy: true, 37 | track_blank_changes: false, 38 | format: nil } 39 | end 40 | 41 | # Sets the :except attributes and relations in `options` to be an [ Array ] 42 | # The attribute names and relations are stored by their `database_field_name`s 43 | # Removes the `nil` and duplicate entries from skipped attributes/relations list 44 | def prepare_skipped_fields 45 | # normalize :except fields to an array of database field strings 46 | @prepared[:except] = Array(@prepared[:except]) 47 | @prepared[:except] = @prepared[:except].map { |field| trackable.database_field_name(field) }.compact.uniq 48 | end 49 | 50 | def prepare_formatted_fields 51 | formats = {} 52 | 53 | if @prepared[:format].class == Hash 54 | @prepared[:format].each do |field, format| 55 | next if field.nil? 56 | 57 | field = trackable.database_field_name(field) 58 | 59 | if format.class == Hash && trackable.embeds_many?(field) 60 | relation_class = trackable.relation_class_of(field) 61 | formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) } 62 | elsif format.class == Hash && trackable.embeds_one?(field) 63 | relation_class = trackable.relation_class_of(field) 64 | formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) } 65 | else 66 | formats[field] = format 67 | end 68 | end 69 | end 70 | 71 | @prepared[:format] = formats 72 | end 73 | 74 | def parse_tracked_fields_and_relations 75 | # case `options[:on]` 76 | # when `posts: [:id, :title]`, then it will convert it to `[[:posts, [:id, :title]]]` 77 | # when `:foo`, then `[:foo]` 78 | # when `[:foo, { posts: [:id, :title] }]`, then return as is 79 | @prepared[:on] = Array(@prepared[:on]) 80 | 81 | @prepared[:on] = @prepared[:on].map { |opt| opt == :all ? :fields : opt } 82 | 83 | if @prepared[:on].include?(:fields) 84 | @prepared[:on] = @prepared[:on].reject { |opt| opt == :fields } 85 | @prepared[:on] = @prepared[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym) 86 | end 87 | 88 | if @prepared[:on].include?(:embedded_relations) 89 | @prepared[:on] = @prepared[:on].reject { |opt| opt == :embedded_relations } 90 | @prepared[:on] = @prepared[:on] | trackable.embedded_relations.keys 91 | end 92 | 93 | @prepared[:fields] = [] 94 | @prepared[:dynamic] = [] 95 | @prepared[:relations] = { embeds_one: {}, embeds_many: {} } 96 | 97 | @prepared[:on].each do |option| 98 | if option.is_a?(Hash) 99 | option.each { |k, v| split_and_categorize(k => v) } 100 | else 101 | split_and_categorize(option) 102 | end 103 | end 104 | end 105 | 106 | def split_and_categorize(field_and_options) 107 | field = get_database_field_name(field_and_options) 108 | field_options = get_field_options(field_and_options) 109 | categorize_tracked_option(field, field_options) 110 | end 111 | 112 | # Returns the database_field_name key for tracked option 113 | # 114 | # @param [ String | Symbol | Array | Hash ] option The field or relation name to track 115 | # 116 | # @return [ String ] the database field name for tracked option 117 | def get_database_field_name(option) 118 | key = if option.is_a?(Hash) 119 | option.keys.first 120 | elsif option.is_a?(Array) 121 | option.first 122 | end 123 | trackable.database_field_name(key || option) 124 | end 125 | 126 | # Returns the tracked attributes for embedded relations, otherwise `nil` 127 | # 128 | # @param [ String | Symbol | Array | Hash ] option The field or relation name to track 129 | # 130 | # @return [ nil | Array ] the list of tracked fields for embedded relation 131 | def get_field_options(option) 132 | if option.is_a?(Hash) 133 | option.values.first 134 | elsif option.is_a?(Array) 135 | option.last 136 | end 137 | end 138 | 139 | # Tracks the passed option under: 140 | # `fields` 141 | # `dynamic` 142 | # `relations -> embeds_one` or 143 | # `relations -> embeds_many` 144 | # 145 | # @param [ String ] field The database field name of field or relation to track 146 | # @param [ nil | Array ] field_options The tracked fields for embedded relations 147 | def categorize_tracked_option(field, field_options = nil) 148 | return if @prepared[:except].include?(field) 149 | return if reserved_fields.include?(field) 150 | 151 | field_options = Array(field_options) 152 | 153 | if trackable.embeds_one?(field) 154 | track_relation(field, :embeds_one, field_options) 155 | elsif trackable.embeds_many?(field) 156 | track_relation(field, :embeds_many, field_options) 157 | elsif trackable.fields.keys.include?(field) 158 | @prepared[:fields] << field 159 | else 160 | @prepared[:dynamic] << field 161 | end 162 | end 163 | 164 | def track_relation(field, kind, field_options) 165 | relation_class = trackable.relation_class_of(field) 166 | @prepared[:relations][kind][field] = if field_options.blank? 167 | relation_class.fields.keys 168 | else 169 | %w[_id] | field_options.map { |opt| relation_class.database_field_name(opt) } 170 | end 171 | end 172 | 173 | def reserved_fields 174 | @reserved_fields ||= ['_id', '_type', @prepared[:version_field].to_s, "#{@prepared[:modifier_field]}_id"] 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/mongoid/history/tracker.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | module Tracker 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | include Mongoid::Document 8 | include Mongoid::Timestamps 9 | attr_writer :trackable 10 | 11 | field :association_chain, type: Array, default: [] 12 | field :modified, type: Hash, default: {} 13 | field :original, type: Hash, default: {} 14 | field :version, type: Integer 15 | field :action, type: String 16 | field :scope, type: String 17 | modifier_options = { 18 | class_name: Mongoid::History.modifier_class_name 19 | } 20 | modifier_options[:optional] = true if Mongoid::Compatibility::Version.mongoid6_or_newer? 21 | belongs_to :modifier, modifier_options 22 | 23 | index(scope: 1) 24 | index(association_chain: 1) 25 | 26 | Mongoid::History.tracker_class_name ||= name.tableize.singularize.to_sym 27 | end 28 | 29 | def undo!(modifier = nil) 30 | if action.to_sym == :destroy 31 | re_create 32 | elsif action.to_sym == :create 33 | re_destroy 34 | elsif Mongoid::Compatibility::Version.mongoid3? 35 | trackable.update_attributes!(undo_attr(modifier), without_protection: true) 36 | else 37 | trackable.update_attributes!(undo_attr(modifier)) 38 | end 39 | end 40 | 41 | def redo!(modifier = nil) 42 | if action.to_sym == :destroy 43 | re_destroy 44 | elsif action.to_sym == :create 45 | re_create 46 | elsif Mongoid::Compatibility::Version.mongoid3? 47 | trackable.update_attributes!(redo_attr(modifier), without_protection: true) 48 | else 49 | trackable.update_attributes!(redo_attr(modifier)) 50 | end 51 | end 52 | 53 | def undo_attr(modifier) 54 | undo_hash = affected.easy_unmerge(modified) 55 | undo_hash.easy_merge!(original) 56 | modifier_field = trackable.history_trackable_options[:modifier_field] 57 | undo_hash[modifier_field] = modifier if modifier_field 58 | (modified.keys - undo_hash.keys).each do |k| 59 | undo_hash[k] = nil 60 | end 61 | localize_keys(undo_hash) 62 | end 63 | 64 | def redo_attr(modifier) 65 | redo_hash = affected.easy_unmerge(original) 66 | redo_hash.easy_merge!(modified) 67 | modifier_field = trackable.history_trackable_options[:modifier_field] 68 | redo_hash[modifier_field] = modifier if modifier_field 69 | localize_keys(redo_hash) 70 | end 71 | 72 | def trackable_root 73 | @trackable_root ||= trackable_parents_and_trackable.first 74 | end 75 | 76 | def trackable 77 | @trackable ||= trackable_parents_and_trackable.last 78 | end 79 | 80 | def trackable_parents 81 | @trackable_parents ||= trackable_parents_and_trackable[0, -1] 82 | end 83 | 84 | def trackable_parent 85 | @trackable_parent ||= trackable_parents_and_trackable[-2] 86 | end 87 | 88 | # Outputs a :from, :to hash for each affected field. Intentionally excludes fields 89 | # which are not tracked, even if there are tracked values for such fields 90 | # present in the database. 91 | # 92 | # @return [ HashWithIndifferentAccess ] a change set in the format: 93 | # { field_1: {to: new_val}, field_2: {from: old_val, to: new_val} } 94 | def tracked_changes 95 | @tracked_changes ||= (modified.keys | original.keys).inject(HashWithIndifferentAccess.new) do |h, k| 96 | h[k] = { from: original[k], to: modified[k] }.delete_if { |_, vv| vv.nil? } 97 | h 98 | end.delete_if { |k, v| v.blank? || !trackable_parent_class.tracked?(k) } 99 | end 100 | 101 | # Outputs summary of edit actions performed: :add, :modify, :remove, or :array. 102 | # Does deep comparison of arrays. Useful for creating human-readable representations 103 | # of the history tracker. Considers changing a value to 'blank' to be a removal. 104 | # 105 | # @return [ HashWithIndifferentAccess ] a change set in the format: 106 | # { add: { field_1: new_val, ... }, 107 | # modify: { field_2: {from: old_val, to: new_val}, ... }, 108 | # remove: { field_3: old_val }, 109 | # array: { field_4: {add: ['foo', 'bar'], remove: ['baz']} } } 110 | def tracked_edits 111 | return @tracked_edits if @tracked_edits 112 | @tracked_edits = HashWithIndifferentAccess.new 113 | 114 | tracked_changes.each do |k, v| 115 | next if v[:from].blank? && v[:to].blank? 116 | 117 | if trackable_parent_class.tracked_embeds_many?(k) 118 | prepare_tracked_edits_for_embeds_many(k, v) 119 | elsif v[:from].blank? 120 | @tracked_edits[:add] ||= {} 121 | @tracked_edits[:add][k] = v[:to] 122 | elsif v[:to].blank? 123 | @tracked_edits[:remove] ||= {} 124 | @tracked_edits[:remove][k] = v[:from] 125 | elsif v[:from].is_a?(Array) && v[:to].is_a?(Array) 126 | @tracked_edits[:array] ||= {} 127 | old_values = v[:from] - v[:to] 128 | new_values = v[:to] - v[:from] 129 | @tracked_edits[:array][k] = { add: new_values, remove: old_values }.delete_if { |_, vv| vv.blank? } 130 | else 131 | @tracked_edits[:modify] ||= {} 132 | @tracked_edits[:modify][k] = v 133 | end 134 | end 135 | @tracked_edits 136 | end 137 | 138 | # Similar to #tracked_changes, but contains only a single value for each 139 | # affected field: 140 | # - :create and :update return the modified values 141 | # - :destroy returns original values 142 | # Included for legacy compatibility. 143 | # 144 | # @deprecated 145 | # 146 | # @return [ HashWithIndifferentAccess ] a change set in the format: 147 | # { field_1: value, field_2: value } 148 | def affected 149 | target = action.to_sym == :destroy ? :from : :to 150 | @affected ||= tracked_changes.inject(HashWithIndifferentAccess.new) do |h, (k, v)| 151 | h[k] = v[target] 152 | h 153 | end 154 | end 155 | 156 | # Returns the class of the trackable, irrespective of whether the trackable object 157 | # has been destroyed. 158 | # 159 | # @return [ Class ] the class of the trackable 160 | def trackable_parent_class 161 | association_chain.first['name'].constantize 162 | end 163 | 164 | private 165 | 166 | def re_create 167 | association_chain.length > 1 ? create_on_parent : create_standalone 168 | end 169 | 170 | def re_destroy 171 | trackable.destroy 172 | end 173 | 174 | def create_standalone 175 | restored = trackable_parent_class.new(localize_keys(original)) 176 | restored.id = original['_id'] 177 | restored.save! 178 | end 179 | 180 | def create_on_parent 181 | name = association_chain.last['name'] 182 | 183 | if trackable_parent.class.embeds_one?(name) 184 | trackable_parent._create_relation(name, localize_keys(original)) 185 | elsif trackable_parent.class.embeds_many?(name) 186 | trackable_parent._get_relation(name).create!(localize_keys(original)) 187 | else 188 | raise 'This should never happen. Please report bug!' 189 | end 190 | end 191 | 192 | def trackable_parents_and_trackable 193 | @trackable_parents_and_trackable ||= traverse_association_chain 194 | end 195 | 196 | def traverse_association_chain 197 | chain = association_chain.dup 198 | doc = nil 199 | documents = [] 200 | loop do 201 | node = chain.shift 202 | name = node['name'] 203 | doc = if doc.nil? 204 | # root association. First element of the association chain 205 | # unscoped is added to remove any default_scope defined in model 206 | klass = name.classify.constantize 207 | klass.unscoped.where(_id: node['id']).first 208 | elsif doc.class.embeds_one?(name) 209 | doc._get_relation(name) 210 | elsif doc.class.embeds_many?(name) 211 | doc._get_relation(name).unscoped.where(_id: node['id']).first 212 | else 213 | relation_klass = doc.class.relation_class_of(name) if doc 214 | relation_klass ||= 'nil' 215 | raise "Unexpected relation for field '#{name}': #{relation_klass}. This should never happen. Please report bug." 216 | end 217 | documents << doc 218 | break if chain.empty? 219 | end 220 | documents 221 | end 222 | 223 | def localize_keys(hash) 224 | klass = association_chain.first['name'].constantize 225 | if klass.respond_to?(:localized_fields) 226 | klass.localized_fields.keys.each do |name| 227 | hash["#{name}_translations"] = hash.delete(name) if hash[name].present? 228 | end 229 | end 230 | hash 231 | end 232 | 233 | def prepare_tracked_edits_for_embeds_many(key, value) 234 | @tracked_edits[:embeds_many] ||= {} 235 | value[:from] ||= [] 236 | value[:to] ||= [] 237 | modify_ids = value[:from].map { |vv| vv['_id'] }.compact & value[:to].map { |vv| vv['_id'] }.compact 238 | modify_values = modify_ids.map { |id| { from: value[:from].detect { |vv| vv['_id'] == id }, to: value[:to].detect { |vv| vv['_id'] == id } } } 239 | modify_values.delete_if { |vv| vv[:from] == vv[:to] } 240 | ignore_values = modify_values.map { |vv| [vv[:from], vv[:to]] }.flatten 241 | old_values = value[:from] - value[:to] - ignore_values 242 | new_values = value[:to] - value[:from] - ignore_values 243 | @tracked_edits[:embeds_many][key] = { add: new_values, remove: old_values, modify: modify_values }.delete_if { |_, vv| vv.blank? } 244 | end 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /lib/mongoid/history/version.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module History 3 | VERSION = '0.9.0'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /mongoid-history.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 2 | require 'mongoid/history/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'mongoid-history' 6 | s.version = Mongoid::History::VERSION 7 | s.authors = ['Aaron Qian', 'Justin Grimes', 'Daniel Doubrovkine'] 8 | s.summary = 'Track and audit, undo and redo changes on Mongoid documents.' 9 | s.description = 'This library 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. Mongoid-history 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 it is probably not suitable for use cases such as a wiki.' 10 | s.email = ['aq1018@gmail.com', 'justin.mgrimes@gmail.com', 'dblock@dblock.org'] 11 | s.homepage = 'http://github.com/mongoid/mongoid-history' 12 | s.license = 'MIT' 13 | 14 | s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 15 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 16 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 17 | s.require_paths = ['lib'] 18 | 19 | s.post_install_message = File.read('UPGRADING') if File.exist?('UPGRADING') 20 | 21 | s.add_runtime_dependency 'easy_diff' 22 | s.add_runtime_dependency 'mongoid', '>= 3.0' 23 | s.add_runtime_dependency 'mongoid-compatibility', '>= 0.5.1' 24 | s.add_runtime_dependency 'activesupport' 25 | end 26 | -------------------------------------------------------------------------------- /perf/benchmark_modified_attributes_for_create.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('../../lib', __FILE__) 2 | 3 | require 'mongoid' 4 | require 'mongoid/history' 5 | 6 | require 'benchmark/ips' 7 | require './perf/gc_suite' 8 | 9 | Mongoid.connect_to('mongoid_history_perf_test') 10 | Mongo::Logger.logger.level = ::Logger::FATAL 11 | Mongoid.purge! 12 | 13 | Attributes = Mongoid::History::Attributes 14 | 15 | module ZeroPointEight 16 | class Create < Attributes::Create 17 | def attributes 18 | @attributes = {} 19 | trackable.attributes.each do |k, v| 20 | next unless trackable_class.tracked_field?(k, :create) 21 | modified = if changes[k] 22 | changes[k].class == Array ? changes[k].last : changes[k] 23 | else 24 | v 25 | end 26 | @attributes[k] = [nil, format_field(k, modified)] 27 | end 28 | insert_embeds_one_changes 29 | insert_embeds_many_changes 30 | @attributes 31 | end 32 | end 33 | end 34 | 35 | class Person 36 | include Mongoid::Document 37 | include Mongoid::History::Trackable 38 | 39 | field :first_name, type: String 40 | field :last_name, type: String 41 | field :birth_date, type: Date 42 | field :title, type: String 43 | 44 | track_history on: %i[first_name last_name birth_date] 45 | end 46 | 47 | new_person = Person.new(first_name: 'Eliot', last_name: 'Horowitz', birth_date: '1981-05-01', title: 'CTO') 48 | 49 | Benchmark.ips do |bm| 50 | bm.config(suite: GCSuite.new) 51 | 52 | bm.report('HEAD') do 53 | Attributes::Create.new(new_person).attributes 54 | end 55 | 56 | bm.report('v0.8.2') do 57 | ZeroPointEight::Create.new(new_person).attributes 58 | end 59 | 60 | bm.report('v0.5.0') do 61 | new_person.attributes.each_with_object({}) { |(k, v), h| h[k] = [nil, v] }.select { |k, _| new_person.class.tracked_field?(k, :create) } 62 | end 63 | 64 | bm.compare! 65 | end 66 | -------------------------------------------------------------------------------- /perf/gc_suite.rb: -------------------------------------------------------------------------------- 1 | class GCSuite 2 | def warming(*) 3 | run_gc 4 | end 5 | 6 | def running(*) 7 | run_gc 8 | end 9 | 10 | def warmup_stats(*); end 11 | 12 | def add_report(*); end 13 | 14 | private 15 | 16 | def run_gc 17 | GC.enable 18 | GC.start 19 | GC.disable 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/integration/embedded_in_polymorphic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | before :each do 5 | class RealState 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | field :name, type: String 10 | belongs_to :user 11 | embeds_one :address, class_name: 'Contact', as: :contactable 12 | embeds_one :embone, as: :embedable 13 | 14 | track_history 15 | end 16 | 17 | class Company 18 | include Mongoid::Document 19 | include Mongoid::History::Trackable 20 | 21 | field :name 22 | belongs_to :user 23 | embeds_one :address, class_name: 'Contact', as: :contactable 24 | embeds_one :second_address, class_name: 'Contact', as: :contactable 25 | embeds_one :embone, as: :embedable 26 | 27 | track_history 28 | end 29 | 30 | class Embone 31 | include Mongoid::Document 32 | include Mongoid::History::Trackable 33 | 34 | field :name 35 | embedded_in :embedable, polymorphic: true 36 | 37 | track_history scope: :embedable 38 | end 39 | 40 | class Contact 41 | include Mongoid::Document 42 | include Mongoid::History::Trackable 43 | 44 | field :address 45 | field :city 46 | field :state 47 | embedded_in :contactable, polymorphic: true 48 | 49 | track_history scope: %i[real_state company] 50 | end 51 | 52 | class User 53 | include Mongoid::Document 54 | has_many :companies, dependent: :destroy 55 | has_many :real_states, dependent: :destroy 56 | end 57 | end 58 | 59 | after :each do 60 | Object.send(:remove_const, :RealState) 61 | Object.send(:remove_const, :Company) 62 | Object.send(:remove_const, :Embone) 63 | Object.send(:remove_const, :Contact) 64 | Object.send(:remove_const, :User) 65 | end 66 | 67 | let!(:user) { User.create! } 68 | 69 | it 'tracks history for nested embedded documents with polymorphic relations' do 70 | real_state = user.real_states.build(name: 'rs_name', modifier: user) 71 | real_state.save! 72 | real_state.build_address(address: 'Main Street #123', city: 'Highland Park', state: 'IL', modifier: user).save! 73 | expect(real_state.history_tracks.count).to eq(2) 74 | expect(real_state.address.history_tracks.count).to eq(1) 75 | 76 | real_state.reload 77 | real_state.address.update_attributes!(address: 'Second Street', modifier: user) 78 | expect(real_state.history_tracks.count).to eq(3) 79 | expect(real_state.address.history_tracks.count).to eq(2) 80 | expect(real_state.history_tracks.last.action).to eq('update') 81 | 82 | real_state.build_embone(name: 'Lorem ipsum', modifier: user).save! 83 | expect(real_state.history_tracks.count).to eq(4) 84 | expect(real_state.embone.history_tracks.count).to eq(1) 85 | expect(real_state.embone.history_tracks.last.action).to eq('create') 86 | expect(real_state.embone.history_tracks.last.association_chain.last['name']).to eq('embone') 87 | 88 | company = user.companies.build(name: 'co_name', modifier: user) 89 | company.save! 90 | company.build_address(address: 'Main Street #456', city: 'Evanston', state: 'IL', modifier: user).save! 91 | expect(company.history_tracks.count).to eq(2) 92 | expect(company.address.history_tracks.count).to eq(1) 93 | 94 | company.reload 95 | company.address.update_attributes!(address: 'Second Street', modifier: user) 96 | expect(company.history_tracks.count).to eq(3) 97 | expect(company.address.history_tracks.count).to eq(2) 98 | expect(company.history_tracks.last.action).to eq('update') 99 | 100 | company.build_second_address(address: 'Main Street #789', city: 'Highland Park', state: 'IL', modifier: user).save! 101 | expect(company.history_tracks.count).to eq(4) 102 | expect(company.second_address.history_tracks.count).to eq(1) 103 | expect(company.second_address.history_tracks.last.action).to eq('create') 104 | expect(company.second_address.history_tracks.last.association_chain.last['name']).to eq('second_address') 105 | 106 | company.build_embone(name: 'Lorem ipsum', modifier: user).save! 107 | expect(company.history_tracks.count).to eq(5) 108 | expect(company.embone.history_tracks.count).to eq(1) 109 | expect(company.embone.history_tracks.last.action).to eq('create') 110 | expect(company.embone.history_tracks.last.association_chain.last['name']).to eq('embone') 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/integration/multi_relation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | before :each do 5 | class Model 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | field :name, type: String 10 | belongs_to :user, inverse_of: :models 11 | has_and_belongs_to_many :external_users, class_name: 'User', inverse_of: :external_models 12 | 13 | track_history on: %i[name user external_user_ids], modifier_field_inverse_of: nil 14 | end 15 | 16 | class User 17 | include Mongoid::Document 18 | 19 | has_many :models, dependent: :destroy, inverse_of: :user 20 | has_and_belongs_to_many :external_model, class_name: 'Model', inverse_of: :external_users 21 | end 22 | end 23 | 24 | after :each do 25 | Object.send(:remove_const, :Model) 26 | Object.send(:remove_const, :User) 27 | end 28 | 29 | let(:user) { User.create! } 30 | let(:model) { Model.create!(name: 'Foo', user: user, modifier: user) } 31 | 32 | it 'should be possible to undo when having multiple relations to modifier class' do 33 | model.update_attributes!(name: 'Bar', modifier: user) 34 | 35 | model.undo! user 36 | expect(model.name).to eq 'Foo' 37 | 38 | model.redo! user, 2 39 | expect(model.name).to eq 'Bar' 40 | end 41 | 42 | it 'should track foreign key relations' do 43 | expect(Model.tracked_field?(:external_user_ids)).to be true 44 | expect(Model.tracked_field?(:user)).to be true 45 | expect(Model.tracked_field?(:user_id)).to be true 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/integration/multiple_trackers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History do 4 | before :each do 5 | class First 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | field :text, type: String 10 | track_history on: [:text], tracker_class_name: :first_history_tracker 11 | end 12 | 13 | class Second 14 | include Mongoid::Document 15 | include Mongoid::History::Trackable 16 | 17 | field :text, type: String 18 | track_history on: [:text], tracker_class_name: :second_history_tracker 19 | end 20 | 21 | class User 22 | include Mongoid::Document 23 | end 24 | 25 | class FirstHistoryTracker 26 | include Mongoid::History::Tracker 27 | end 28 | 29 | class SecondHistoryTracker 30 | include Mongoid::History::Tracker 31 | end 32 | end 33 | 34 | after :each do 35 | Object.send(:remove_const, :First) 36 | Object.send(:remove_const, :Second) 37 | Object.send(:remove_const, :User) 38 | Object.send(:remove_const, :FirstHistoryTracker) 39 | Object.send(:remove_const, :SecondHistoryTracker) 40 | end 41 | 42 | let(:user) { User.create! } 43 | 44 | it 'should be possible to have different trackers for each class' do 45 | expect(FirstHistoryTracker.count).to eq(0) 46 | expect(SecondHistoryTracker.count).to eq(0) 47 | expect(First.tracker_class).to be FirstHistoryTracker 48 | expect(Second.tracker_class).to be SecondHistoryTracker 49 | 50 | foo = First.create!(modifier: user) 51 | bar = Second.create!(modifier: user) 52 | 53 | expect(FirstHistoryTracker.count).to eq 1 54 | expect(SecondHistoryTracker.count).to eq 1 55 | 56 | foo.update_attributes!(text: "I'm foo") 57 | bar.update_attributes!(text: "I'm bar") 58 | 59 | expect(FirstHistoryTracker.count).to eq 2 60 | expect(SecondHistoryTracker.count).to eq 2 61 | 62 | foo.destroy 63 | bar.destroy 64 | 65 | expect(FirstHistoryTracker.count).to eq 3 66 | expect(SecondHistoryTracker.count).to eq 3 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/integration/nested_embedded_documents_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | before :each do 5 | class ModelOne 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | field :name, type: String 10 | belongs_to :user, inverse_of: :model_ones 11 | embeds_many :emb_ones 12 | 13 | track_history 14 | end 15 | 16 | class EmbOne 17 | include Mongoid::Document 18 | include Mongoid::History::Trackable 19 | 20 | field :name 21 | embeds_many :emb_twos, store_as: :ems 22 | embedded_in :model_one 23 | 24 | track_history 25 | end 26 | 27 | class EmbTwo 28 | include Mongoid::Document 29 | include Mongoid::History::Trackable 30 | 31 | field :name 32 | embedded_in :emb_one 33 | 34 | track_history scope: :model_one 35 | end 36 | 37 | class User 38 | include Mongoid::Document 39 | 40 | has_many :model_ones, dependent: :destroy, inverse_of: :user 41 | end 42 | end 43 | 44 | after :each do 45 | Object.send(:remove_const, :ModelOne) 46 | Object.send(:remove_const, :EmbOne) 47 | Object.send(:remove_const, :EmbTwo) 48 | Object.send(:remove_const, :User) 49 | end 50 | 51 | let(:user) { User.create! } 52 | 53 | it 'should be able to track history for nested embedded documents' do 54 | model = ModelOne.create!(name: 'm1name', user: user, modifier: user) 55 | embedded1 = model.emb_ones.create!(name: 'e1name', modifier: user) 56 | embedded2 = embedded1.emb_twos.create!(name: 'e2name', modifier: user) 57 | 58 | embedded2.update_attributes!(name: 'a new name') 59 | 60 | model.history_tracks[-1].undo! user 61 | expect(embedded1.reload.name).to eq('e1name') 62 | expect(embedded2.reload.name).to eq('e2name') 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/integration/nested_embedded_documents_tracked_in_parent_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | describe 'Tracking of changes from embedded documents' do 5 | before :each do 6 | # Child model (will be embedded in Parent) 7 | class Child 8 | include Mongoid::Document 9 | include Mongoid::History::Trackable 10 | 11 | field :name 12 | embedded_in :parent, inverse_of: :child 13 | embeds_one :child, inverse_of: :parent, class_name: 'NestedChild' 14 | end 15 | 16 | # NestedChild model (will be embedded in Child) 17 | class NestedChild 18 | include Mongoid::Document 19 | include Mongoid::History::Trackable 20 | 21 | field :name 22 | embedded_in :parent, inverse_of: :child, class_name: 'Child' 23 | end 24 | 25 | # Parent model (embeds one Child) 26 | class Parent 27 | include Mongoid::Document 28 | include Mongoid::History::Trackable 29 | 30 | field :name, type: String 31 | embeds_one :child 32 | 33 | store_in collection: :parent 34 | 35 | track_history( 36 | on: %i[fields embedded_relations], 37 | version_field: :version, 38 | track_create: true, 39 | track_update: true, 40 | track_destroy: false, 41 | modifier_field: nil 42 | ) 43 | end 44 | end 45 | 46 | after :each do 47 | Object.send(:remove_const, :Parent) 48 | Object.send(:remove_const, :Child) 49 | Object.send(:remove_const, :NestedChild) 50 | end 51 | 52 | context 'with a parent-child hierarchy' do 53 | let(:parent) do 54 | Parent.create!(name: 'bowser', child: Child.new(name: 'todd')) 55 | end 56 | 57 | it 'tracks history for the nested embedded documents in the parent' do 58 | expect(parent.history_tracks.length).to eq(1) 59 | 60 | aggregate_failures do 61 | track = parent.history_tracks.last 62 | expect(track.modified['name']).to eq('bowser') 63 | expect(track.modified.dig('child', 'name')).to eq('todd') 64 | end 65 | 66 | parent.update_attributes(name: 'brow') 67 | expect(parent.history_tracks.length).to eq(2) 68 | 69 | parent.child.name = 'mario' 70 | parent.save! 71 | expect(parent.history_tracks.length).to eq(3) 72 | 73 | aggregate_failures do 74 | track = parent.history_tracks.last 75 | expect(track.original.dig('child', 'name')).to eq('todd') 76 | expect(track.modified.dig('child', 'name')).to eq('mario') 77 | end 78 | end 79 | end 80 | 81 | context 'with a deeply nested hierarchy' do 82 | let(:parent) do 83 | Parent.create!( 84 | name: 'bowser', 85 | child: Child.new( 86 | name: 'todd', 87 | child: NestedChild.new(name: 'peach') 88 | ) 89 | ) 90 | end 91 | 92 | it 'tracks history for deeply nested embedded documents in parent' do 93 | pending('Figure out a way to track deeply nested relation changes') 94 | 95 | expect(parent.history_tracks.length).to eq(1) 96 | 97 | aggregate_failures do 98 | track = parent.history_tracks.last 99 | expect(track.modified['name']).to eq('bowser') 100 | expect(track.modified.dig('child', 'name')).to eq('todd') 101 | expect(track.modified.dig('child', 'child', 'name')).to eq('peach') 102 | end 103 | 104 | parent.name = 'brow' 105 | parent.child.name = 'mario' 106 | parent.child.child.name = 'luigi' 107 | parent.save! 108 | expect(parent.history_tracks.length).to eq(2) 109 | 110 | aggregate_failures do 111 | track = parent.history_tracks.last 112 | expect(track.original['name']).to eq('bowser') 113 | expect(track.modified['name']).to eq('brow') 114 | 115 | expect(track.original['child']['name']).to eq('todd') 116 | expect(track.modified['child']['name']).to eq('mario') 117 | 118 | expect(track.original['child']['child']['name']).to eq('peach') 119 | expect(track.modified['child']['child']['name']).to eq('luigi') 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/integration/nested_embedded_polymorphic_documents_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | before :each do 5 | class ModelOne 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | field :name, type: String 10 | belongs_to :user 11 | embeds_one :one_embedded, as: :embedable 12 | 13 | track_history 14 | end 15 | 16 | class ModelTwo 17 | include Mongoid::Document 18 | include Mongoid::History::Trackable 19 | 20 | field :name, type: String 21 | belongs_to :user 22 | embeds_one :one_embedded, as: :embedable 23 | 24 | track_history 25 | end 26 | 27 | class OneEmbedded 28 | include Mongoid::Document 29 | include Mongoid::History::Trackable 30 | 31 | field :name 32 | embeds_many :embedded_twos, store_as: :ems 33 | embedded_in :embedable, polymorphic: true 34 | 35 | track_history scope: %i[model_one model_two] 36 | end 37 | 38 | class EmbeddedTwo 39 | include Mongoid::Document 40 | include Mongoid::History::Trackable 41 | 42 | field :name 43 | embedded_in :one_embedded 44 | 45 | track_history scope: %i[model_one model_two] 46 | end 47 | 48 | class User 49 | include Mongoid::Document 50 | 51 | has_many :model_ones 52 | has_many :model_twos 53 | end 54 | end 55 | 56 | after :each do 57 | Object.send(:remove_const, :ModelOne) 58 | Object.send(:remove_const, :ModelTwo) 59 | Object.send(:remove_const, :OneEmbedded) 60 | Object.send(:remove_const, :EmbeddedTwo) 61 | Object.send(:remove_const, :User) 62 | end 63 | 64 | let (:user) { User.create! } 65 | 66 | it 'tracks history for nested embedded documents with polymorphic relations' do 67 | user = User.create! 68 | 69 | model_one = user.model_ones.build(name: 'model_one', modifier: user) 70 | model_one.save! 71 | model_one.build_one_embedded(name: 'model_one_one_embedded', modifier: user).save! 72 | expect(model_one.history_tracks.count).to eq(2) 73 | expect(model_one.one_embedded.history_tracks.count).to eq(1) 74 | 75 | model_one.reload 76 | model_one.one_embedded.update_attributes!(name: 'model_one_embedded_one!') 77 | expect(model_one.history_tracks.count).to eq(3) 78 | expect(model_one.one_embedded.history_tracks.count).to eq(2) 79 | expect(model_one.history_tracks.last.action).to eq('update') 80 | 81 | model_one.build_one_embedded(name: 'Lorem ipsum', modifier: user).save! 82 | expect(model_one.history_tracks.count).to eq(4) 83 | expect(model_one.one_embedded.history_tracks.count).to eq(1) 84 | expect(model_one.one_embedded.history_tracks.last.action).to eq('create') 85 | expect(model_one.one_embedded.history_tracks.last.association_chain.last['name']).to eq('one_embedded') 86 | 87 | embedded_one1 = model_one.one_embedded.embedded_twos.create!(name: 'model_one_one_embedded_1', modifier: user) 88 | expect(model_one.history_tracks.count).to eq(5) 89 | expect(model_one.one_embedded.history_tracks.count).to eq(2) 90 | expect(embedded_one1.history_tracks.count).to eq(1) 91 | 92 | model_two = user.model_twos.build(name: 'model_two', modifier: user) 93 | model_two.save! 94 | model_two.build_one_embedded(name: 'model_two_one_embedded', modifier: user).save! 95 | expect(model_two.history_tracks.count).to eq(2) 96 | expect(model_two.one_embedded.history_tracks.count).to eq(1) 97 | 98 | model_two.reload 99 | model_two.one_embedded.update_attributes!(name: 'model_two_one_embedded!') 100 | expect(model_two.history_tracks.count).to eq(3) 101 | expect(model_two.one_embedded.history_tracks.count).to eq(2) 102 | expect(model_two.history_tracks.last.action).to eq('update') 103 | 104 | model_two.build_one_embedded(name: 'Lorem ipsum', modifier: user).save! 105 | expect(model_two.history_tracks.count).to eq(4) 106 | expect(model_two.one_embedded.history_tracks.count).to eq(1) 107 | expect(model_two.one_embedded.history_tracks.last.action).to eq('create') 108 | expect(model_two.one_embedded.history_tracks.last.association_chain.last['name']).to eq('one_embedded') 109 | 110 | embedded_one2 = model_two.one_embedded.embedded_twos.create!(name: 'model_two_one_embedded_1', modifier: user) 111 | expect(model_two.history_tracks.count).to eq(5) 112 | expect(model_two.one_embedded.history_tracks.count).to eq(2) 113 | expect(embedded_one2.history_tracks.count).to eq(1) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/integration/subclasses_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | before :each do 5 | class Element 6 | include Mongoid::Document 7 | include Mongoid::Timestamps 8 | include Mongoid::History::Trackable 9 | 10 | track_history 11 | 12 | field :body 13 | 14 | # force preparation of options 15 | history_trackable_options 16 | end 17 | 18 | class Prompt < Element 19 | field :head 20 | end 21 | 22 | class User 23 | include Mongoid::Document 24 | end 25 | end 26 | 27 | after :each do 28 | Object.send(:remove_const, :Element) 29 | Object.send(:remove_const, :Prompt) 30 | Object.send(:remove_const, :User) 31 | end 32 | 33 | let(:user) { User.create! } 34 | 35 | it 'tracks subclass create and update' do 36 | prompt = Prompt.new(modifier: user) 37 | expect { prompt.save! }.to change(Tracker, :count).by(1) 38 | expect { prompt.update_attributes!(body: 'one', head: 'two') }.to change(Tracker, :count).by(1) 39 | prompt.undo! user 40 | expect(prompt.body).to be_blank 41 | expect(prompt.head).to be_blank 42 | prompt.redo! user, 2 43 | expect(prompt.body).to eq('one') 44 | expect(prompt.head).to eq('two') 45 | expect { prompt.destroy }.to change(Tracker, :count).by(1) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/integration/track_history_order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | context 'when track_history not called' do 5 | before :each do 6 | class NotModel 7 | include Mongoid::Document 8 | include Mongoid::History::Trackable 9 | 10 | field :foo 11 | end 12 | end 13 | 14 | after :each do 15 | Object.send(:remove_const, :NotModel) 16 | end 17 | 18 | it 'should not track fields' do 19 | expect(NotModel.respond_to?(:tracked?)).to be false 20 | end 21 | end 22 | 23 | context 'boefore field' do 24 | before :each do 25 | class InsideBeforeModel 26 | include Mongoid::Document 27 | include Mongoid::History::Trackable 28 | 29 | track_history on: :fields 30 | 31 | field :foo 32 | end 33 | end 34 | 35 | after :each do 36 | Object.send(:remove_const, :InsideBeforeModel) 37 | end 38 | 39 | it 'should track fields' do 40 | expect(InsideBeforeModel.tracked?(:foo)).to be true 41 | end 42 | end 43 | 44 | context 'when track_history called inside class and after fields' do 45 | before :each do 46 | class InsideAfterModel 47 | include Mongoid::Document 48 | include Mongoid::History::Trackable 49 | 50 | field :foo 51 | 52 | track_history on: :fields 53 | end 54 | end 55 | 56 | after :each do 57 | Object.send(:remove_const, :InsideAfterModel) 58 | end 59 | 60 | it 'should track fields' do 61 | expect(InsideAfterModel.tracked?(:foo)).to be true 62 | end 63 | end 64 | 65 | context 'when track_history called outside class' do 66 | before :each do 67 | class OutsideModel 68 | include Mongoid::Document 69 | include Mongoid::History::Trackable 70 | 71 | field :foo 72 | end 73 | end 74 | 75 | after :each do 76 | Object.send(:remove_const, :OutsideModel) 77 | end 78 | 79 | it 'should track fields' do 80 | OutsideModel.track_history on: :fields 81 | expect(OutsideModel.tracked?(:foo)).to be true 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/integration/validation_failure_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | before :each do 5 | class Element 6 | include Mongoid::Document 7 | include Mongoid::Timestamps 8 | include Mongoid::History::Trackable 9 | 10 | field :title 11 | field :body 12 | 13 | validates :title, presence: true 14 | 15 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 16 | has_many :items, dependent: :restrict_with_exception 17 | else 18 | has_many :items, dependent: :restrict 19 | end 20 | 21 | track_history on: [:body] 22 | end 23 | 24 | class Item 25 | include Mongoid::Document 26 | include Mongoid::Timestamps 27 | 28 | belongs_to :element 29 | end 30 | 31 | class Prompt < Element 32 | end 33 | 34 | class User 35 | include Mongoid::Document 36 | end 37 | end 38 | 39 | after :each do 40 | Object.send(:remove_const, :Element) 41 | Object.send(:remove_const, :Item) 42 | Object.send(:remove_const, :Prompt) 43 | Object.send(:remove_const, :User) 44 | end 45 | 46 | let(:user) { User.create! } 47 | 48 | it 'does not track delete when parent class validation fails' do 49 | prompt = Prompt.new(title: 'first', modifier: user) 50 | expect { prompt.save! }.to change(Tracker, :count).by(1) 51 | expect do 52 | expect { prompt.update_attributes!(title: nil, body: 'one') } 53 | .to raise_error(Mongoid::Errors::Validations) 54 | end.to change(Tracker, :count).by(0) 55 | end 56 | 57 | it 'does not track delete when parent class restrict dependency fails' do 58 | prompt = Prompt.new(title: 'first', modifier: user) 59 | prompt.items << Item.new 60 | expect { prompt.save! }.to change(Tracker, :count).by(1) 61 | expect(prompt.version).to eq(1) 62 | expect do 63 | expect { prompt.destroy }.to raise_error(Mongoid::Errors::DeleteRestriction) 64 | end.to change(Tracker, :count).by(0) 65 | end 66 | 67 | it 'does not track delete when restrict dependency fails' do 68 | elem = Element.new(title: 'first', modifier: user) 69 | elem.items << Item.new 70 | expect { elem.save! }.to change(Tracker, :count).by(1) 71 | expect(elem.version).to eq(1) 72 | expect do 73 | expect { elem.destroy }.to raise_error(Mongoid::Errors::DeleteRestriction) 74 | end.to change(Tracker, :count).by(0) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | $LOAD_PATH.push File.expand_path('../../lib', __FILE__) 5 | 6 | require 'active_support/all' 7 | require 'mongoid' 8 | require 'request_store' 9 | 10 | # Undefine RequestStore so that it may be stubbed in specific tests 11 | RequestStoreTemp = RequestStore 12 | Object.send(:remove_const, :RequestStore) 13 | 14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 15 | 16 | require 'mongoid/history' 17 | 18 | RSpec.configure do |config| 19 | config.raise_errors_for_deprecations! 20 | 21 | config.before :all do 22 | Mongoid.logger.level = Logger::INFO 23 | Mongo::Logger.logger.level = Logger::INFO if Mongoid::Compatibility::Version.mongoid5_or_newer? 24 | Mongoid.belongs_to_required_by_default = false if Mongoid::Compatibility::Version.mongoid6? 25 | end 26 | 27 | config.before :each do 28 | Mongoid::History.reset! 29 | end 30 | 31 | config.include ErrorHelpers 32 | end 33 | -------------------------------------------------------------------------------- /spec/support/error_helpers.rb: -------------------------------------------------------------------------------- 1 | module ErrorHelpers 2 | def ignore_errors 3 | yield 4 | rescue StandardError => e 5 | Mongoid.logger.debug "ignored error #{e}" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/mongoid.rb: -------------------------------------------------------------------------------- 1 | Mongoid.configure do |config| 2 | config.connect_to('mongoid_history_test') 3 | end 4 | 5 | RSpec.configure do |config| 6 | config.after(:each) do 7 | Mongoid.purge! 8 | end 9 | 10 | config.backtrace_exclusion_patterns = [%r{lib\/rspec\/(core|expectations|matchers|mocks)}] 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/mongoid_history.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before :each do 3 | class Tracker 4 | include Mongoid::History::Tracker 5 | end 6 | Mongoid::History.tracker_class_name = 'Tracker' 7 | Mongoid::History.modifier_class_name = 'User' 8 | end 9 | config.after :each do 10 | Mongoid::History.tracker_class_name = nil 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/unit/attributes/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Attributes::Base do 4 | before :each do 5 | class ModelOne 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | field :foo 10 | field :b, as: :bar 11 | end 12 | 13 | class ModelTwo 14 | include Mongoid::Document 15 | 16 | field :foo 17 | field :goo 18 | end 19 | end 20 | 21 | after :each do 22 | Object.send(:remove_const, :ModelOne) 23 | Object.send(:remove_const, :ModelTwo) 24 | end 25 | 26 | let(:obj_one) { ModelOne.new } 27 | let(:base) { described_class.new(obj_one) } 28 | subject { base } 29 | 30 | it { is_expected.to respond_to(:trackable) } 31 | 32 | describe '#initialize' do 33 | it { expect(base.instance_variable_get(:@trackable)).to eq obj_one } 34 | end 35 | 36 | describe '#trackable_class' do 37 | subject { base.send(:trackable_class) } 38 | it { is_expected.to eq ModelOne } 39 | end 40 | 41 | describe '#aliased_fields' do 42 | subject { base.send(:aliased_fields) } 43 | it { is_expected.to eq('id' => '_id', 'bar' => 'b') } 44 | end 45 | 46 | describe '#changes_method' do 47 | before(:each) do 48 | ModelOne.track_history changes_method: :my_changes 49 | end 50 | subject { base.send(:changes_method) } 51 | it { is_expected.to eq :my_changes } 52 | end 53 | 54 | describe '#changes' do 55 | before(:each) do 56 | ModelOne.track_history 57 | allow(obj_one).to receive(:changes) { { 'foo' => ['Foo', 'Foo-new'] } } 58 | end 59 | subject { base.send(:changes) } 60 | it { is_expected.to eq('foo' => ['Foo', 'Foo-new']) } 61 | end 62 | 63 | describe '#format_field' do 64 | subject { base.send(:format_field, :bar, 'foo') } 65 | 66 | context 'when formatted via string' do 67 | before do 68 | ModelOne.track_history format: { bar: '*%s*' } 69 | end 70 | 71 | it { is_expected.to eq '*foo*' } 72 | end 73 | 74 | context 'when formatted via proc' do 75 | before do 76 | ModelOne.track_history format: { bar: ->(v) { v * 2 } } 77 | end 78 | 79 | it { is_expected.to eq 'foofoo' } 80 | end 81 | 82 | context 'when not formatted' do 83 | before do 84 | ModelOne.track_history 85 | end 86 | 87 | it { is_expected.to eq 'foo' } 88 | end 89 | end 90 | 91 | shared_examples 'formats embedded relation' do |relation_type| 92 | let(:model_two) { ModelTwo.new(foo: :bar, goo: :baz) } 93 | 94 | before :each do 95 | ModelOne.send(relation_type, :model_two) 96 | end 97 | 98 | subject { base.send("format_#{relation_type}_relation", :model_two, model_two.attributes) } 99 | 100 | context 'with permitted attributes' do 101 | before do 102 | ModelOne.track_history on: { model_two: %i[foo] } 103 | end 104 | 105 | it 'should select only permitted attributes' do 106 | is_expected.to include('foo' => :bar) 107 | is_expected.to_not include('goo') 108 | end 109 | end 110 | 111 | context 'with attributes formatted via string' do 112 | before do 113 | ModelOne.track_history on: { model_two: %i[foo] }, format: { model_two: { foo: '&%s&' } } 114 | end 115 | 116 | it 'should select obfuscate permitted attributes' do 117 | is_expected.to include('foo' => '&bar&') 118 | is_expected.to_not include('goo') 119 | end 120 | end 121 | 122 | context 'with attributes formatted via proc' do 123 | before do 124 | ModelOne.track_history on: { model_two: %i[foo] }, format: { model_two: { foo: ->(v) { v.to_s * 2 } } } 125 | end 126 | 127 | it 'should select obfuscate permitted attributes' do 128 | is_expected.to include('foo' => 'barbar') 129 | is_expected.to_not include('goo') 130 | end 131 | end 132 | end 133 | 134 | describe '#format_embeds_one_relation' do 135 | include_examples 'formats embedded relation', :embeds_one 136 | end 137 | 138 | describe '#format_embeds_many_relation' do 139 | include_examples 'formats embedded relation', :embeds_many 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/unit/attributes/create_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Attributes::Create do 4 | before :each do 5 | class ModelOne 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | store_in collection: :model_ones 10 | field :foo 11 | field :b, as: :bar 12 | 13 | track_history on: :foo 14 | end 15 | end 16 | 17 | after :each do 18 | Object.send(:remove_const, :ModelOne) 19 | end 20 | 21 | let(:base) { described_class.new(obj_one) } 22 | subject { base } 23 | 24 | describe '#attributes' do 25 | subject { base.attributes } 26 | 27 | describe 'fields' do 28 | let(:obj_one) { ModelOne.new } 29 | let(:obj_one) { ModelOne.new(foo: 'Foo', bar: 'Bar') } 30 | it { is_expected.to eq('foo' => [nil, 'Foo']) } 31 | end 32 | 33 | describe '#insert_embeds_one_changes' do 34 | context 'when untracked relation' do 35 | before :each do 36 | class ModelTwo 37 | include Mongoid::Document 38 | include Mongoid::History::Trackable 39 | 40 | store_in collection: :model_twos 41 | 42 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 43 | embeds_one :emb_one_one 44 | else 45 | embeds_one :emb_one_one, inverse_class_name: 'EmbOneOne' 46 | end 47 | 48 | track_history on: :fields 49 | end 50 | 51 | class EmbOneOne 52 | include Mongoid::Document 53 | 54 | field :em_bar 55 | embedded_in :model_one 56 | end 57 | end 58 | 59 | after :each do 60 | Object.send(:remove_const, :ModelTwo) 61 | Object.send(:remove_const, :EmbOneOne) 62 | end 63 | 64 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 65 | let(:emb_obj) { EmbOneOne.new(em_bar: 'Em-Bar') } 66 | 67 | it { is_expected.to eq({}) } 68 | end 69 | 70 | context 'when tracked relation' do 71 | before :each do 72 | class ModelTwo 73 | include Mongoid::Document 74 | include Mongoid::History::Trackable 75 | 76 | store_in collection: :model_twos 77 | 78 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 79 | embeds_one :emb_one_one 80 | else 81 | embeds_one :emb_one_one, inverse_class_name: 'EmbOneOne' 82 | end 83 | 84 | track_history on: :emb_one_one 85 | end 86 | 87 | class EmbOneOne 88 | include Mongoid::Document 89 | 90 | field :em_bar 91 | embedded_in :model_one 92 | end 93 | end 94 | 95 | after :each do 96 | Object.send(:remove_const, :ModelTwo) 97 | Object.send(:remove_const, :EmbOneOne) 98 | end 99 | 100 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 101 | let(:emb_obj) { EmbOneOne.new(em_bar: 'Em-Bar') } 102 | 103 | it { is_expected.to eq('emb_one_one' => [nil, { '_id' => emb_obj._id, 'em_bar' => 'Em-Bar' }]) } 104 | end 105 | 106 | context 'when paranoia_field without alias' do 107 | before :each do 108 | class ModelTwo 109 | include Mongoid::Document 110 | include Mongoid::History::Trackable 111 | 112 | store_in collection: :model_twos 113 | 114 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 115 | embeds_one :emb_one_one 116 | else 117 | embeds_one :emb_one_one, inverse_class_name: 'EmbOneOne' 118 | end 119 | 120 | track_history on: :emb_one_one 121 | end 122 | 123 | class EmbOneOne 124 | include Mongoid::Document 125 | include Mongoid::History::Trackable 126 | 127 | field :em_bar 128 | field :removed_at 129 | 130 | embedded_in :model_one 131 | 132 | history_settings paranoia_field: :removed_at 133 | end 134 | end 135 | 136 | after :each do 137 | Object.send(:remove_const, :ModelTwo) 138 | Object.send(:remove_const, :EmbOneOne) 139 | end 140 | 141 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 142 | let(:emb_obj) { EmbOneOne.new(em_bar: 'Em-Bar', removed_at: Time.now) } 143 | 144 | it { is_expected.to eq({}) } 145 | end 146 | 147 | context 'when paranoia_field with alias' do 148 | before :each do 149 | class ModelTwo 150 | include Mongoid::Document 151 | include Mongoid::History::Trackable 152 | 153 | store_in collection: :model_twos 154 | 155 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 156 | embeds_one :emb_one_one 157 | else 158 | embeds_one :emb_one_one, inverse_class_name: 'EmbOneOne' 159 | end 160 | 161 | track_history on: :emb_one_one 162 | end 163 | 164 | class EmbOneOne 165 | include Mongoid::Document 166 | include Mongoid::History::Trackable 167 | 168 | field :em_bar 169 | field :rmvt, as: :removed_at 170 | 171 | embedded_in :model_one 172 | 173 | history_settings paranoia_field: :removed_at 174 | end 175 | end 176 | 177 | after :each do 178 | Object.send(:remove_const, :ModelTwo) 179 | Object.send(:remove_const, :EmbOneOne) 180 | end 181 | 182 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 183 | let(:emb_obj) { EmbOneOne.new(em_bar: 'Em-Bar', removed_at: Time.now) } 184 | 185 | it { is_expected.to eq({}) } 186 | end 187 | 188 | context 'with permitted attributes' do 189 | before :each do 190 | class ModelTwo 191 | include Mongoid::Document 192 | include Mongoid::History::Trackable 193 | 194 | store_in collection: :model_twos 195 | 196 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 197 | embeds_one :emb_one_one 198 | else 199 | embeds_one :emb_one_one, inverse_class_name: 'EmbOneOne' 200 | end 201 | 202 | track_history on: [{ emb_one_one: :em_bar }] 203 | end 204 | 205 | class EmbOneOne 206 | include Mongoid::Document 207 | include Mongoid::History::Trackable 208 | 209 | field :em_foo 210 | field :em_bar 211 | 212 | embedded_in :model_one 213 | end 214 | end 215 | 216 | after :each do 217 | Object.send(:remove_const, :ModelTwo) 218 | Object.send(:remove_const, :EmbOneOne) 219 | end 220 | 221 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 222 | let(:emb_obj) { EmbOneOne.new(em_foo: 'Em-Foo', em_bar: 'Em-Bar') } 223 | 224 | it { is_expected.to eq('emb_one_one' => [nil, { '_id' => emb_obj._id, 'em_bar' => 'Em-Bar' }]) } 225 | end 226 | 227 | context 'when relation with alias' do 228 | before :each do 229 | class ModelTwo 230 | include Mongoid::Document 231 | include Mongoid::History::Trackable 232 | 233 | store_in collection: :model_twos 234 | 235 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 236 | embeds_one :emb_one_one, store_as: :eoo 237 | else 238 | embeds_one :emb_one_one, inverse_class_name: 'EmbOneOne', store_as: :eoo 239 | end 240 | 241 | track_history on: :emb_one_one 242 | end 243 | 244 | class EmbOneOne 245 | include Mongoid::Document 246 | 247 | field :em_bar 248 | embedded_in :model_one 249 | end 250 | end 251 | 252 | after :each do 253 | Object.send(:remove_const, :ModelTwo) 254 | Object.send(:remove_const, :EmbOneOne) 255 | end 256 | 257 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 258 | let(:emb_obj) { EmbOneOne.new(em_bar: 'Em-Bar') } 259 | 260 | it { is_expected.to eq('emb_one_one' => [nil, { '_id' => emb_obj._id, 'em_bar' => 'Em-Bar' }]) } 261 | end 262 | 263 | context 'when no object' do 264 | before :each do 265 | class ModelTwo 266 | include Mongoid::Document 267 | include Mongoid::History::Trackable 268 | 269 | store_in collection: :model_twos 270 | 271 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 272 | embeds_one :emb_one_one, store_as: :eoo 273 | else 274 | embeds_one :emb_one_one, store_as: :eoo, inverse_class_name: 'EmbOneOne' 275 | end 276 | 277 | track_history on: :emb_one_one 278 | end 279 | 280 | class EmbOneOne 281 | include Mongoid::Document 282 | 283 | field :em_bar 284 | embedded_in :model_one 285 | end 286 | end 287 | 288 | after :each do 289 | Object.send(:remove_const, :ModelTwo) 290 | Object.send(:remove_const, :EmbOneOne) 291 | end 292 | 293 | let(:obj_one) { ModelTwo.new } 294 | 295 | it { is_expected.to eq({}) } 296 | end 297 | 298 | context 'when object not paranoid' do 299 | before :each do 300 | class ModelTwo 301 | include Mongoid::Document 302 | include Mongoid::History::Trackable 303 | 304 | store_in collection: :model_twos 305 | 306 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 307 | embeds_one :emb_one_one, store_as: :eoo 308 | else 309 | embeds_one :emb_one_one, store_as: :eoo, inverse_class_name: 'EmbOneOne' 310 | end 311 | 312 | track_history on: :emb_one_one 313 | end 314 | 315 | class EmbOneOne 316 | include Mongoid::Document 317 | include Mongoid::History::Trackable 318 | 319 | field :em_bar 320 | field :cancelled_at 321 | 322 | embedded_in :model_one 323 | 324 | history_settings paranoia_field: :cancelled_at 325 | end 326 | end 327 | 328 | after :each do 329 | Object.send(:remove_const, :ModelTwo) 330 | Object.send(:remove_const, :EmbOneOne) 331 | end 332 | 333 | let(:obj_one) { ModelTwo.new(emb_one_one: emb_obj) } 334 | let(:emb_obj) { EmbOneOne.new(em_bar: 'Em-Bar') } 335 | 336 | it { is_expected.to eq('emb_one_one' => [nil, { '_id' => emb_obj._id, 'em_bar' => 'Em-Bar' }]) } 337 | end 338 | end 339 | 340 | pending '#insert_embeds_many_changes' 341 | end 342 | end 343 | -------------------------------------------------------------------------------- /spec/unit/attributes/destroy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Attributes::Destroy do 4 | before :each do 5 | class ModelOne 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | store_in collection: :model_ones 10 | 11 | field :foo 12 | field :b, as: :bar 13 | 14 | track_history on: :foo, modifier_field_optional: true 15 | end 16 | end 17 | 18 | after :each do 19 | Object.send(:remove_const, :ModelOne) 20 | end 21 | 22 | let(:obj_one) { ModelOne.new } 23 | let(:base) { described_class.new(obj_one) } 24 | subject { base } 25 | 26 | describe '#attributes' do 27 | subject { base.attributes } 28 | 29 | describe '#fields' do 30 | before :each do 31 | obj_one.save! 32 | end 33 | 34 | let(:obj_one) { ModelOne.new(foo: 'Foo', bar: 'Bar') } 35 | it { is_expected.to eq('_id' => [obj_one._id, nil], 'foo' => ['Foo', nil], 'version' => [1, nil]) } 36 | end 37 | 38 | describe '#insert_embeds_one_changes' do 39 | before :each do 40 | class ModelTwo 41 | include Mongoid::Document 42 | include Mongoid::History::Trackable 43 | 44 | store_in collection: :model_twos 45 | 46 | embeds_one :emb_two 47 | 48 | track_history on: :fields, modifier_field_optional: true 49 | end 50 | 51 | class EmbTwo 52 | include Mongoid::Document 53 | 54 | field :em_foo 55 | field :em_bar 56 | 57 | embedded_in :model_two 58 | end 59 | end 60 | 61 | after :each do 62 | Object.send(:remove_const, :ModelTwo) 63 | Object.send(:remove_const, :EmbTwo) 64 | end 65 | 66 | let(:obj_two) { ModelTwo.new(emb_two: emb_obj_two) } 67 | let(:emb_obj_two) { EmbTwo.new(em_foo: 'Em-Foo', em_bar: 'Em-Bar') } 68 | let(:base) { described_class.new(obj_two) } 69 | 70 | context 'when relation tracked' do 71 | before :each do 72 | ModelTwo.track_history on: :emb_two, modifier_field_optional: true 73 | obj_two.save! 74 | end 75 | it { expect(subject['emb_two']).to eq [{ '_id' => emb_obj_two._id, 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }, nil] } 76 | end 77 | 78 | context 'when relation not tracked' do 79 | before :each do 80 | ModelTwo.track_history on: :fields, modifier_field_optional: true 81 | allow(ModelTwo).to receive(:dynamic_enabled?) { false } 82 | obj_two.save! 83 | end 84 | it { expect(subject['emb_two']).to be_nil } 85 | end 86 | 87 | context 'when relation with alias' do 88 | before :each do 89 | class ModelThree 90 | include Mongoid::Document 91 | include Mongoid::History::Trackable 92 | 93 | store_in collection: :model_threes 94 | embeds_one :emb_three, store_as: :emtr 95 | 96 | track_history on: :emb_three, modifier_field_optional: true 97 | end 98 | 99 | class EmbThree 100 | include Mongoid::Document 101 | 102 | field :em_foo 103 | embedded_in :model_three 104 | end 105 | end 106 | 107 | after :each do 108 | Object.send(:remove_const, :ModelThree) 109 | Object.send(:remove_const, :EmbThree) 110 | end 111 | 112 | before :each do 113 | obj_three.save! 114 | end 115 | 116 | let(:obj_three) { ModelThree.new(emb_three: emb_obj_three) } 117 | let(:emb_obj_three) { EmbThree.new(em_foo: 'Em-Foo') } 118 | let(:base) { described_class.new(obj_three) } 119 | 120 | it { expect(subject['emb_three']).to eq [{ '_id' => emb_obj_three._id, 'em_foo' => 'Em-Foo' }, nil] } 121 | end 122 | 123 | context 'relation with permitted attributes' do 124 | before :each do 125 | ModelTwo.track_history on: [{ emb_two: :em_foo }], modifier_field_optional: true 126 | obj_two.save! 127 | end 128 | 129 | it { expect(subject['emb_two']).to eq [{ '_id' => emb_obj_two._id, 'em_foo' => 'Em-Foo' }, nil] } 130 | end 131 | 132 | context 'when relation object not built' do 133 | before :each do 134 | ModelTwo.track_history on: :emb_two, modifier_field_optional: true 135 | obj_two.save! 136 | end 137 | 138 | let(:obj_two) { ModelTwo.new } 139 | it { expect(subject['emb_two']).to be_nil } 140 | end 141 | end 142 | 143 | describe '#insert_embeds_many_changes' do 144 | context 'Case 1:' do 145 | before :each do 146 | class ModelTwo 147 | include Mongoid::Document 148 | include Mongoid::History::Trackable 149 | 150 | embeds_many :em_twos 151 | track_history on: :fields 152 | end 153 | 154 | class EmTwo 155 | include Mongoid::Document 156 | 157 | field :em_foo 158 | field :em_bar 159 | 160 | embedded_in :model_two 161 | end 162 | end 163 | 164 | after :each do 165 | Object.send(:remove_const, :ModelTwo) 166 | Object.send(:remove_const, :EmTwo) 167 | end 168 | 169 | let(:obj_two) { ModelTwo.new(em_twos: [em_obj_two]) } 170 | let(:em_obj_two) { EmTwo.new(em_foo: 'Em-Foo', em_bar: 'Em-Bar') } 171 | let(:base) { described_class.new(obj_two) } 172 | 173 | context 'when relation tracked' do 174 | before :each do 175 | ModelTwo.track_history on: :em_twos 176 | end 177 | it { expect(subject['em_twos']).to eq [[{ '_id' => em_obj_two._id, 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }], nil] } 178 | end 179 | 180 | context 'when relation not tracked' do 181 | before :each do 182 | ModelTwo.track_history on: :fields 183 | end 184 | it { expect(subject['em_twos']).to be_nil } 185 | end 186 | 187 | context 'when relation with permitted attributes for tracking' do 188 | before :each do 189 | ModelTwo.track_history on: { em_twos: :em_foo } 190 | end 191 | it { expect(subject['em_twos']).to eq [[{ '_id' => em_obj_two._id, 'em_foo' => 'Em-Foo' }], nil] } 192 | end 193 | end 194 | 195 | context 'when relation with alias' do 196 | before :each do 197 | class ModelTwo 198 | include Mongoid::Document 199 | include Mongoid::History::Trackable 200 | 201 | embeds_many :em_twos, store_as: :emws 202 | track_history on: :fields 203 | 204 | track_history on: :em_twos 205 | end 206 | 207 | class EmTwo 208 | include Mongoid::Document 209 | 210 | field :em_foo 211 | embedded_in :model_two 212 | end 213 | end 214 | 215 | after :each do 216 | Object.send(:remove_const, :ModelTwo) 217 | Object.send(:remove_const, :EmTwo) 218 | end 219 | 220 | let(:obj_two) { ModelTwo.new(em_twos: [em_obj_two]) } 221 | let(:em_obj_two) { EmTwo.new(em_foo: 'Em-Foo') } 222 | let(:base) { described_class.new(obj_two) } 223 | 224 | it { expect(subject['em_twos']).to eq [[{ '_id' => em_obj_two._id, 'em_foo' => 'Em-Foo' }], nil] } 225 | end 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /spec/unit/attributes/update_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Attributes::Update do 4 | describe '#attributes' do 5 | describe '#insert_embeds_one_changes' do 6 | context 'Case: relation without alias' do 7 | before :each do 8 | class ModelOne 9 | include Mongoid::Document 10 | include Mongoid::History::Trackable 11 | 12 | store_in collection: :model_ones 13 | embeds_one :emb_one 14 | 15 | track_history on: :fields 16 | end 17 | 18 | class EmbOne 19 | include Mongoid::Document 20 | 21 | field :em_foo 22 | field :em_bar 23 | 24 | embedded_in :model_one 25 | end 26 | end 27 | 28 | after :each do 29 | Object.send(:remove_const, :ModelOne) 30 | Object.send(:remove_const, :EmbOne) 31 | end 32 | 33 | before :each do 34 | allow(base).to receive(:changes) { changes } 35 | end 36 | 37 | let(:obj_one) { ModelOne.new } 38 | let(:base) { described_class.new(obj_one) } 39 | let(:changes) do 40 | { 'emb_one' => [{ 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }, { 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }] } 41 | end 42 | subject { base.attributes } 43 | 44 | context 'with permitted attributes' do 45 | before :each do 46 | ModelOne.track_history on: { emb_one: :em_foo } 47 | end 48 | it { expect(subject['emb_one']).to eq [{ 'em_foo' => 'Em-Foo' }, { 'em_foo' => 'Em-Foo-new' }] } 49 | end 50 | 51 | context 'without permitted attributes' do 52 | before :each do 53 | ModelOne.track_history on: :emb_one 54 | end 55 | it { expect(subject['emb_one']).to eq [{ 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }, { 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }] } 56 | end 57 | 58 | context 'when old value soft-deleted' do 59 | before :each do 60 | ModelOne.track_history on: :emb_one 61 | end 62 | let(:changes) do 63 | { 'emb_one' => [{ 'em_foo' => 'Em-Foo', 'deleted_at' => Time.now }, { 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }] } 64 | end 65 | it { expect(subject['emb_one']).to eq [{}, { 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }] } 66 | end 67 | 68 | context 'when new value soft-deleted' do 69 | before :each do 70 | ModelOne.track_history on: :emb_one 71 | end 72 | let(:changes) do 73 | { 'emb_one' => [{ 'em_foo' => 'Em-Foo' }, { 'em_foo' => 'Em-Foo-new', 'deleted_at' => Time.now }] } 74 | end 75 | it { expect(subject['emb_one']).to eq [{ 'em_foo' => 'Em-Foo' }, {}] } 76 | end 77 | 78 | context 'when not tracked' do 79 | before :each do 80 | ModelOne.track_history on: :fields 81 | allow(ModelOne).to receive(:dynamic_enabled?) { false } 82 | end 83 | it { expect(subject['emb_one']).to be_nil } 84 | end 85 | end 86 | 87 | context 'Case: relation with alias' do 88 | before :each do 89 | class ModelOne 90 | include Mongoid::Document 91 | include Mongoid::History::Trackable 92 | store_in collection: :model_ones 93 | embeds_one :emb_one, store_as: :eon 94 | track_history on: :fields 95 | end 96 | 97 | class EmbOne 98 | include Mongoid::Document 99 | field :em_foo 100 | field :em_bar 101 | embedded_in :model_one 102 | end 103 | end 104 | 105 | after :each do 106 | Object.send(:remove_const, :ModelOne) 107 | Object.send(:remove_const, :EmbOne) 108 | end 109 | 110 | before :each do 111 | ModelOne.track_history on: :emb_one 112 | allow(base).to receive(:changes) { changes } 113 | end 114 | 115 | let(:obj_one) { ModelOne.new } 116 | let(:base) { described_class.new(obj_one) } 117 | let(:changes) do 118 | { 'emb_one' => [{ 'em_foo' => 'Em-Foo' }, { 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }] } 119 | end 120 | subject { base.attributes } 121 | it { expect(subject['eon']).to eq [{ 'em_foo' => 'Em-Foo' }, { 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }] } 122 | end 123 | 124 | context 'when original and modified value same' do 125 | before :each do 126 | class DummyUpdateModel 127 | include Mongoid::Document 128 | include Mongoid::History::Trackable 129 | store_in collection: :dummy_update_models 130 | embeds_one :dummy_embedded_model 131 | track_history on: :fields 132 | end 133 | 134 | class DummyEmbeddedModel 135 | include Mongoid::Document 136 | field :em_foo 137 | field :em_bar 138 | embedded_in :dummy_update_model 139 | end 140 | end 141 | 142 | after :each do 143 | Object.send(:remove_const, :DummyUpdateModel) 144 | Object.send(:remove_const, :DummyEmbeddedModel) 145 | end 146 | 147 | before :each do 148 | allow(base).to receive(:changes) { changes } 149 | DummyUpdateModel.track_history on: :dummy_embedded_model 150 | end 151 | 152 | let(:obj_one) { DummyUpdateModel.new } 153 | let(:base) { described_class.new(obj_one) } 154 | let(:changes) do 155 | { 'dummy_embedded_model' => [{ 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }, { 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }] } 156 | end 157 | subject { base.attributes } 158 | it { expect(subject.keys).to_not include 'dummy_embedded_model' } 159 | end 160 | end 161 | 162 | describe '#insert_embeds_many_changes' do 163 | context 'Case: relation without alias' do 164 | before :each do 165 | class ModelOne 166 | include Mongoid::Document 167 | include Mongoid::History::Trackable 168 | store_in collection: :model_ones 169 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 170 | embeds_many :emb_ones 171 | else 172 | embeds_many :emb_ones, inverse_class_name: 'EmbOne' 173 | end 174 | track_history on: :fields 175 | end 176 | 177 | class EmbOne 178 | include Mongoid::Document 179 | field :em_foo 180 | field :em_bar 181 | embedded_in :model_one 182 | end 183 | end 184 | 185 | before :each do 186 | allow(base).to receive(:changes) { changes } 187 | end 188 | 189 | let(:obj_one) { ModelOne.new } 190 | let(:base) { described_class.new(obj_one) } 191 | subject { base.attributes } 192 | 193 | context 'with whitelist attributes' do 194 | before :each do 195 | ModelOne.track_history on: { emb_ones: :em_foo } 196 | end 197 | let(:changes) do 198 | { 'emb_ones' => [[{ 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }], [{ 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }]] } 199 | end 200 | it 'should track only whitelisted attributes' do 201 | expect(subject['emb_ones']).to eq [[{ 'em_foo' => 'Em-Foo' }], [{ 'em_foo' => 'Em-Foo-new' }]] 202 | end 203 | end 204 | 205 | context 'without whitelist attributes' do 206 | before :each do 207 | ModelOne.track_history(on: :emb_ones) 208 | end 209 | let(:changes) do 210 | { 'emb_ones' => [[{ 'em_foo' => 'Em-Foo', 'deleted_at' => Time.now }], [{ 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }]] } 211 | end 212 | it 'should ignore soft-deleted objects' do 213 | expect(subject['emb_ones']).to eq [[], [{ 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }]] 214 | end 215 | end 216 | 217 | after :each do 218 | Object.send(:remove_const, :ModelOne) 219 | Object.send(:remove_const, :EmbOne) 220 | end 221 | end 222 | 223 | context 'Case: relation with alias' do 224 | before :each do 225 | class ModelOne 226 | include Mongoid::Document 227 | include Mongoid::History::Trackable 228 | store_in collection: :model_ones 229 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 230 | embeds_many :emb_ones, store_as: :eons 231 | else 232 | embeds_many :emb_ones, store_as: :eons, inverse_class_name: 'EmbOne' 233 | end 234 | track_history on: :fields 235 | end 236 | 237 | class EmbOne 238 | include Mongoid::Document 239 | field :em_foo 240 | field :em_bar 241 | embedded_in :model_one 242 | end 243 | end 244 | 245 | before :each do 246 | ModelOne.track_history on: :emb_ones 247 | allow(base).to receive(:changes) { changes } 248 | end 249 | 250 | let(:obj_one) { ModelOne.new } 251 | let(:base) { described_class.new(obj_one) } 252 | let(:changes) do 253 | { 'emb_ones' => [[{ 'em_foo' => 'Em-Foo' }], [{ 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }]] } 254 | end 255 | subject { base.attributes } 256 | it 'should save audit history under relation alias' do 257 | expect(subject['eons']).to eq [[{ 'em_foo' => 'Em-Foo' }], [{ 'em_foo' => 'Em-Foo-new', 'em_bar' => 'Em-Bar-new' }]] 258 | end 259 | 260 | after :each do 261 | Object.send(:remove_const, :ModelOne) 262 | Object.send(:remove_const, :EmbOne) 263 | end 264 | end 265 | 266 | context 'when original and modified value same' do 267 | before :each do 268 | class ModelOne 269 | include Mongoid::Document 270 | include Mongoid::History::Trackable 271 | store_in collection: :model_ones 272 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 273 | embeds_many :emb_ones 274 | else 275 | embeds_many :emb_ones, inverse_class_name: 'EmbOne' 276 | end 277 | track_history on: :fields 278 | end 279 | 280 | class EmbOne 281 | include Mongoid::Document 282 | field :em_foo 283 | field :em_bar 284 | embedded_in :model_one 285 | end 286 | end 287 | 288 | before :each do 289 | allow(base).to receive(:changes) { changes } 290 | ModelOne.track_history on: :emb_ones 291 | end 292 | 293 | let(:obj_one) { ModelOne.new } 294 | let(:base) { described_class.new(obj_one) } 295 | let(:changes) do 296 | { 'emb_ones' => [[{ 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }], [{ 'em_foo' => 'Em-Foo', 'em_bar' => 'Em-Bar' }]] } 297 | end 298 | subject { base.attributes } 299 | it { expect(subject.keys).to_not include 'emb_ones' } 300 | 301 | after :each do 302 | Object.send(:remove_const, :ModelOne) 303 | Object.send(:remove_const, :EmbOne) 304 | end 305 | end 306 | end 307 | 308 | [false, true].each do |original_nil| 309 | context "when original value #{original_nil ? 'nil' : 'blank'} and modified value #{original_nil ? 'blank' : 'nil'}" do 310 | [nil, false, true].each do |track_blank_changes| 311 | context "when track_blank_changes #{track_blank_changes.nil? ? 'default' : track_blank_changes}" do 312 | before :each do 313 | class DummyParent 314 | include Mongoid::Document 315 | include Mongoid::History::Trackable 316 | store_in collection: :dummy_parents 317 | has_and_belongs_to_many :other_dummy_parents 318 | field :boolean, type: Boolean 319 | field :string, type: String 320 | field :hash, type: Hash 321 | end 322 | 323 | class OtherDummyParent 324 | include Mongoid::Document 325 | has_and_belongs_to_many :dummy_parents 326 | end 327 | 328 | if track_blank_changes.nil? 329 | DummyParent.track_history on: :fields 330 | else 331 | DummyParent.track_history \ 332 | on: :fields, 333 | track_blank_changes: track_blank_changes 334 | end 335 | 336 | allow(base).to receive(:changes) { changes } 337 | end 338 | 339 | after :each do 340 | Object.send(:remove_const, :DummyParent) 341 | Object.send(:remove_const, :OtherDummyParent) 342 | end 343 | 344 | let(:base) { described_class.new(DummyParent.new) } 345 | subject { base.attributes.keys } 346 | 347 | # These can't be memoizing methods (i.e. lets) because of limits 348 | # on where those can be used. 349 | 350 | cmp = track_blank_changes ? 'should' : 'should_not' 351 | cmp_name = cmp.humanize capitalize: false 352 | 353 | [ 354 | { n: 'many-to-many', f: 'other_dummy_parent_ids', v: [] }, 355 | { n: 'boolean', f: 'boolean', v: false }, 356 | { n: 'empty string', f: 'string', v: '' }, 357 | { n: 'all whitespace string', f: 'string', v: " \t\n\r\f\v" } 358 | # The second character in that string is an actual tab (0x9). 359 | ].each do |d| 360 | context "#{d[:n]} field" do 361 | let(:changes) do 362 | { d[:f] => original_nil ? [nil, d[:v]] : [d[:v], nil] } 363 | end 364 | it "changes #{cmp_name} include #{d[:f]}" do 365 | send(cmp, include(d[:f])) 366 | end 367 | end 368 | end 369 | end 370 | end 371 | end 372 | end 373 | end 374 | end 375 | -------------------------------------------------------------------------------- /spec/unit/callback_options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Options do 4 | describe ':if' do 5 | before :each do 6 | class DummyModel 7 | include Mongoid::Document 8 | include Mongoid::History::Trackable 9 | 10 | store_in collection: :model_ones 11 | field :foo 12 | 13 | attr_accessor :bar 14 | 15 | track_history modifier_field_optional: true, if: :bar 16 | end 17 | end 18 | 19 | after :each do 20 | Object.send(:remove_const, :DummyModel) 21 | end 22 | 23 | let(:obj) { DummyModel.new(foo: 'Foo') } 24 | 25 | context 'when condition evaluates to true' do 26 | before { obj.bar = true } 27 | it 'should create history tracks' do 28 | expect { obj.save }.to change(Tracker, :count).by(1) 29 | expect do 30 | obj.update_attributes(foo: 'Foo-2') 31 | end.to change(Tracker, :count).by(1) 32 | expect { obj.destroy }.to change(Tracker, :count).by(1) 33 | end 34 | end 35 | 36 | context 'when condition evaluates to false' do 37 | before { obj.bar = false } 38 | it 'should not create history tracks' do 39 | expect { obj.save }.to_not change(Tracker, :count) 40 | expect do 41 | obj.update_attributes(foo: 'Foo-2') 42 | end.to_not change(Tracker, :count) 43 | expect { obj.destroy }.to_not change(Tracker, :count) 44 | end 45 | end 46 | end 47 | 48 | describe ':unless' do 49 | before :each do 50 | class DummyModel 51 | include Mongoid::Document 52 | include Mongoid::History::Trackable 53 | 54 | store_in collection: :model_ones 55 | field :foo 56 | 57 | attr_accessor :bar 58 | 59 | track_history modifier_field_optional: true, unless: ->(obj) { obj.bar } 60 | end 61 | end 62 | 63 | after :each do 64 | Object.send(:remove_const, :DummyModel) 65 | end 66 | 67 | let(:obj) { DummyModel.new(foo: 'Foo') } 68 | 69 | context 'when condition evaluates to true' do 70 | before { obj.bar = true } 71 | it 'should not create history tracks' do 72 | expect { obj.save }.to_not change(Tracker, :count) 73 | expect do 74 | obj.update_attributes(foo: 'Foo-2') 75 | end.to_not change(Tracker, :count) 76 | expect { obj.destroy }.to_not change(Tracker, :count) 77 | end 78 | end 79 | 80 | context 'when condition evaluates to false' do 81 | before { obj.bar = false } 82 | it 'should create history tracks' do 83 | expect { obj.save }.to change(Tracker, :count).by(1) 84 | expect do 85 | obj.update_attributes(foo: 'Foo-2') 86 | end.to change(Tracker, :count).by(1) 87 | expect { obj.destroy }.to change(Tracker, :count).by(1) 88 | end 89 | end 90 | end 91 | 92 | describe ':if and :unless' do 93 | before :each do 94 | class DummyModel 95 | include Mongoid::Document 96 | include Mongoid::History::Trackable 97 | 98 | store_in collection: :model_ones 99 | field :foo 100 | 101 | attr_accessor :bar, :baz 102 | 103 | track_history modifier_field_optional: true, if: :bar, unless: ->(obj) { obj.baz } 104 | end 105 | end 106 | 107 | after :each do 108 | Object.send(:remove_const, :DummyModel) 109 | end 110 | 111 | let(:obj) { DummyModel.new(foo: 'Foo') } 112 | 113 | context 'when :if condition evaluates to true' do 114 | before { obj.bar = true } 115 | 116 | context 'and :unless condition evaluates to true' do 117 | before { obj.baz = true } 118 | it 'should not create history tracks' do 119 | expect { obj.save }.to_not change(Tracker, :count) 120 | expect do 121 | obj.update_attributes(foo: 'Foo-2') 122 | end.to_not change(Tracker, :count) 123 | expect { obj.destroy }.to_not change(Tracker, :count) 124 | end 125 | end 126 | 127 | context 'and :unless condition evaluates to false' do 128 | before { obj.baz = false } 129 | it 'should create history tracks' do 130 | expect { obj.save }.to change(Tracker, :count).by(1) 131 | expect do 132 | obj.update_attributes(foo: 'Foo-2') 133 | end.to change(Tracker, :count).by(1) 134 | expect { obj.destroy }.to change(Tracker, :count).by(1) 135 | end 136 | end 137 | end 138 | 139 | context 'when :if condition evaluates to false' do 140 | before { obj.bar = false } 141 | 142 | context 'and :unless condition evaluates to true' do 143 | before { obj.baz = true } 144 | it 'should not create history tracks' do 145 | expect { obj.save }.to_not change(Tracker, :count) 146 | expect do 147 | obj.update_attributes(foo: 'Foo-2') 148 | end.to_not change(Tracker, :count) 149 | expect { obj.destroy }.to_not change(Tracker, :count) 150 | end 151 | end 152 | 153 | context 'and :unless condition evaluates to false' do 154 | before { obj.baz = false } 155 | it 'should not create history tracks' do 156 | expect { obj.save }.to_not change(Tracker, :count) 157 | expect do 158 | obj.update_attributes(foo: 'Foo-2') 159 | end.to_not change(Tracker, :count) 160 | expect { obj.destroy }.to_not change(Tracker, :count) 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/unit/embedded_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Trackable do 4 | describe 'EmbeddedMethods' do 5 | describe 'relation_class_of' do 6 | before :each do 7 | class ModelOne 8 | include Mongoid::Document 9 | include Mongoid::History::Trackable 10 | 11 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 12 | embeds_one :emb_one 13 | embeds_one :emb_two, store_as: 'emt' 14 | else 15 | embeds_one :emb_one, inverse_class_name: 'EmbOne' 16 | embeds_one :emb_two, store_as: 'emt', inverse_class_name: 'EmbTwo' 17 | end 18 | 19 | track_history 20 | end 21 | 22 | class EmbOne 23 | include Mongoid::Document 24 | 25 | embedded_in :model_one 26 | end 27 | 28 | class EmbTwo 29 | include Mongoid::Document 30 | 31 | embedded_in :model_one 32 | end 33 | end 34 | 35 | after :each do 36 | Object.send(:remove_const, :ModelOne) 37 | Object.send(:remove_const, :EmbOne) 38 | Object.send(:remove_const, :EmbTwo) 39 | end 40 | 41 | it { expect(ModelOne.relation_class_of('emb_one')).to eq EmbOne } 42 | it { expect(ModelOne.relation_class_of('emt')).to eq EmbTwo } 43 | it { expect(ModelOne.relation_class_of('invalid')).to be_nil } 44 | end 45 | 46 | describe 'relation_class_of' do 47 | before :each do 48 | class ModelOne 49 | include Mongoid::Document 50 | include Mongoid::History::Trackable 51 | 52 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 53 | embeds_many :emb_ones 54 | embeds_many :emb_twos, store_as: 'emts' 55 | else 56 | embeds_many :emb_ones, inverse_class_name: 'EmbOne' 57 | embeds_many :emb_twos, store_as: 'emts', inverse_class_name: 'EmbTwo' 58 | end 59 | 60 | track_history 61 | end 62 | 63 | class EmbOne 64 | include Mongoid::Document 65 | 66 | embedded_in :model_one 67 | end 68 | 69 | class EmbTwo 70 | include Mongoid::Document 71 | 72 | embedded_in :model_one 73 | end 74 | end 75 | 76 | after :each do 77 | Object.send(:remove_const, :ModelOne) 78 | Object.send(:remove_const, :EmbOne) 79 | Object.send(:remove_const, :EmbTwo) 80 | end 81 | 82 | it { expect(ModelOne.relation_class_of('emb_ones')).to eq EmbOne } 83 | it { expect(ModelOne.relation_class_of('emts')).to eq EmbTwo } 84 | it { expect(ModelOne.relation_class_of('invalid')).to be_nil } 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/unit/history_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History do 4 | it { is_expected.to respond_to(:trackable_settings) } 5 | it { is_expected.to respond_to(:trackable_settings=) } 6 | 7 | describe '#default_settings' do 8 | let(:default_settings) { { paranoia_field: 'deleted_at' } } 9 | it { expect(described_class.default_settings).to eq(default_settings) } 10 | end 11 | 12 | describe '#trackable_class_settings' do 13 | before :each do 14 | class ModelOne 15 | include Mongoid::Document 16 | include Mongoid::History::Trackable 17 | 18 | store_in collection: :model_ones 19 | end 20 | end 21 | 22 | after :each do 23 | Object.send(:remove_const, :ModelOne) 24 | end 25 | 26 | context 'when present' do 27 | before :each do 28 | ModelOne.history_settings paranoia_field: :annuled_at 29 | end 30 | it { expect(described_class.trackable_class_settings(ModelOne)).to eq(paranoia_field: 'annuled_at') } 31 | end 32 | 33 | context 'when not present' do 34 | it { expect(described_class.trackable_class_settings(ModelOne)).to eq(paranoia_field: 'deleted_at') } 35 | end 36 | end 37 | 38 | describe '#reset!' do 39 | before :each do 40 | class ModelTwo 41 | include Mongoid::Document 42 | include Mongoid::History::Trackable 43 | 44 | track_history 45 | end 46 | end 47 | 48 | after :each do 49 | Object.send(:remove_const, :ModelTwo) 50 | end 51 | 52 | it 'should remove all configurations' do 53 | expect(ModelTwo).to have_attributes mongoid_history_options: be_a(Mongoid::History::Options) 54 | Mongoid::History.reset! 55 | expect(ModelTwo).to_not respond_to :mongoid_history_options 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/unit/my_instance_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Trackable do 4 | describe 'MyInstanceMethods' do 5 | before :each do 6 | class ModelOne 7 | include Mongoid::Document 8 | include Mongoid::History::Trackable 9 | 10 | store_in collection: :model_ones 11 | 12 | field :foo 13 | field :b, as: :bar 14 | 15 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 16 | embeds_one :emb_one 17 | embeds_one :emb_two, store_as: :emt 18 | embeds_many :emb_threes 19 | embeds_many :emb_fours, store_as: :emfs 20 | else 21 | embeds_one :emb_one, inverse_class_name: 'EmbOne' 22 | embeds_one :emb_two, store_as: :emt, inverse_class_name: 'EmbTwo' 23 | embeds_many :emb_threes, inverse_class_name: 'EmbThree' 24 | embeds_many :emb_fours, store_as: :emfs, inverse_class_name: 'EmbFour' 25 | end 26 | end 27 | 28 | class EmbOne 29 | include Mongoid::Document 30 | include Mongoid::History::Trackable 31 | 32 | field :f_em_foo 33 | field :fmb, as: :f_em_bar 34 | 35 | embedded_in :model_one 36 | end 37 | 38 | class EmbTwo 39 | include Mongoid::Document 40 | include Mongoid::History::Trackable 41 | 42 | field :baz 43 | embedded_in :model_one 44 | end 45 | 46 | class EmbThree 47 | include Mongoid::Document 48 | include Mongoid::History::Trackable 49 | 50 | field :f_em_foo 51 | field :fmb, as: :f_em_bar 52 | 53 | embedded_in :model_one 54 | end 55 | 56 | class EmbFour 57 | include Mongoid::Document 58 | include Mongoid::History::Trackable 59 | 60 | field :baz 61 | embedded_in :model_one 62 | end 63 | end 64 | 65 | after :each do 66 | Object.send(:remove_const, :ModelOne) 67 | Object.send(:remove_const, :EmbOne) 68 | Object.send(:remove_const, :EmbTwo) 69 | Object.send(:remove_const, :EmbThree) 70 | Object.send(:remove_const, :EmbFour) 71 | end 72 | 73 | let(:bson_class) { defined?(BSON::ObjectId) ? BSON::ObjectId : Moped::BSON::ObjectId } 74 | 75 | let(:emb_one) { EmbOne.new(f_em_foo: 'Foo', f_em_bar: 'Bar') } 76 | let(:emb_threes) { [EmbThree.new(f_em_foo: 'Foo', f_em_bar: 'Bar')] } 77 | let(:model_one) { ModelOne.new(foo: 'Foo', bar: 'Bar', emb_one: emb_one, emb_threes: emb_threes) } 78 | 79 | describe '#modified_attributes_for_create' do 80 | before :each do 81 | ModelOne.track_history modifier_field_optional: true, on: %i[foo emb_one emb_threes] 82 | end 83 | 84 | subject { model_one.send(:modified_attributes_for_create) } 85 | 86 | context 'with tracked embeds_one object' do 87 | before :each do 88 | ModelOne.track_history(modifier_field_optional: true, on: { emb_one: :f_em_foo }) 89 | end 90 | it 'should include tracked attributes only' do 91 | expect(subject['emb_one'][0]).to be_nil 92 | 93 | expect(subject['emb_one'][1].keys.size).to eq 2 94 | expect(subject['emb_one'][1]['_id']).to eq emb_one._id 95 | expect(subject['emb_one'][1]['f_em_foo']).to eq 'Foo' 96 | end 97 | end 98 | 99 | context 'with untracked embeds_one object' do 100 | before :each do 101 | ModelOne.track_history(modifier_field_optional: true, on: :fields) 102 | end 103 | it 'should not include embeds_one attributes' do 104 | expect(subject['emb_one']).to be_nil 105 | end 106 | end 107 | 108 | context 'with tracked embeds_many objects' do 109 | before :each do 110 | ModelOne.track_history(modifier_field_optional: true, on: { emb_threes: :f_em_foo }) 111 | end 112 | it 'should include tracked attributes only' do 113 | expect(subject['emb_threes'][0]).to be_nil 114 | 115 | expect(subject['emb_threes'][1][0].keys.count).to eq 2 116 | expect(subject['emb_threes'][1][0]['_id']).to eq emb_threes.first._id 117 | expect(subject['emb_threes'][1][0]['f_em_foo']).to eq 'Foo' 118 | end 119 | end 120 | 121 | context 'with untracked embeds_many objects' do 122 | before :each do 123 | ModelOne.track_history(modifier_field_optional: true, on: :fields) 124 | end 125 | it 'should include not tracked embeds_many attributes' do 126 | expect(subject['emb_threes']).to be_nil 127 | end 128 | end 129 | 130 | context 'when embeds_one object blank' do 131 | let(:emb_one) { nil } 132 | 133 | it 'should not include embeds_one model key' do 134 | expect(subject.keys).to_not include 'emb_one' 135 | end 136 | end 137 | 138 | describe 'embeds_one' do 139 | before :each do 140 | class Mail 141 | include Mongoid::Document 142 | include Mongoid::History::Trackable 143 | 144 | store_in collection: :mails 145 | field :provider 146 | 147 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 148 | embeds_one :mail_subject 149 | else 150 | embeds_one :mail_subject, inverse_class_name: 'MailSubject' 151 | end 152 | 153 | track_history on: :mail_subject 154 | end 155 | 156 | class MailSubject 157 | include Mongoid::Document 158 | 159 | field :content 160 | embedded_in :mail 161 | end 162 | end 163 | 164 | after :each do 165 | Object.send(:remove_const, :MailSubject) 166 | Object.send(:remove_const, :Mail) 167 | end 168 | 169 | let(:mail) { Mail.new(mail_subject: mail_subject) } 170 | let(:mail_subject) { nil } 171 | subject { mail.send(:modified_attributes_for_create)['mail_subject'] } 172 | 173 | context 'when obj not built' do 174 | it { is_expected.to be_nil } 175 | end 176 | 177 | context 'when obj does not respond to paranoia_field' do 178 | let(:mail_subject) { MailSubject.new(content: 'Content') } 179 | it { is_expected.to eq [nil, { '_id' => mail_subject._id, 'content' => 'Content' }] } 180 | end 181 | 182 | context 'when obj not soft-deleted' do 183 | before :each do 184 | allow(mail_subject).to receive(:deleted_at) { nil } 185 | end 186 | let(:mail_subject) { MailSubject.new(content: 'Content') } 187 | it { is_expected.to eq [nil, { '_id' => mail_subject._id, 'content' => 'Content' }] } 188 | end 189 | 190 | context 'when obj soft-deleted' do 191 | before :each do 192 | allow(mail_subject).to receive(:deleted_at) { Time.now } 193 | end 194 | let(:mail_subject) { MailSubject.new(content: 'Content') } 195 | it { is_expected.to be_nil } 196 | end 197 | end 198 | 199 | describe 'paranoia' do 200 | before :each do 201 | class ModelParanoia 202 | include Mongoid::Document 203 | include Mongoid::History::Trackable 204 | 205 | store_in collection: :model_paranoias 206 | 207 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 208 | embeds_many :emb_para_ones 209 | else 210 | embeds_many :emb_para_ones, inverse_class_name: 'EmbParaOne' 211 | end 212 | end 213 | 214 | class EmbParaOne 215 | include Mongoid::Document 216 | 217 | field :em_foo 218 | field :deleted_at 219 | 220 | embedded_in :model_paranoia 221 | end 222 | end 223 | 224 | after :each do 225 | Object.send(:remove_const, :ModelParanoia) 226 | Object.send(:remove_const, :EmbParaOne) 227 | end 228 | 229 | let(:emb_para_one) { EmbParaOne.new(em_foo: 'Em-Foo') } 230 | let(:model_paranoia) { ModelParanoia.new(emb_para_ones: [emb_para_one]) } 231 | 232 | context 'when does not respond to paranoia_field' do 233 | before :each do 234 | ModelParanoia.track_history(on: :emb_para_ones) 235 | end 236 | 237 | subject { model_paranoia.send(:modified_attributes_for_create) } 238 | 239 | it 'should include tracked embeds_many objects attributes' do 240 | expect(subject['emb_para_ones'][0]).to be_nil 241 | expect(subject['emb_para_ones'][1].size).to eq 1 242 | expect(subject['emb_para_ones'][1][0]['_id']).to be_a bson_class 243 | expect(subject['emb_para_ones'][1][0]['em_foo']).to eq 'Em-Foo' 244 | end 245 | end 246 | 247 | context 'when responds to paranoia_field' do 248 | before :each do 249 | ModelParanoia.track_history(on: :emb_para_ones) 250 | allow(emb_para_one).to receive(:deleted_at) { Time.now } 251 | allow(emb_para_one_2).to receive(:deleted_at) { nil } 252 | end 253 | 254 | let(:model_paranoia) { ModelParanoia.new(emb_para_ones: [emb_para_one, emb_para_one_2]) } 255 | let(:emb_para_one) { EmbParaOne.new(em_foo: 'Em-Foo') } 256 | let(:emb_para_one_2) { EmbParaOne.new(em_foo: 'Em-Foo-2') } 257 | 258 | subject { model_paranoia.send(:modified_attributes_for_create) } 259 | 260 | it 'should not include deleted objects attributes' do 261 | expect(subject['emb_para_ones'][0]).to be_nil 262 | expect(subject['emb_para_ones'][1]).to eq [{ '_id' => emb_para_one_2._id, 'em_foo' => 'Em-Foo-2' }] 263 | end 264 | end 265 | end 266 | end 267 | 268 | describe '#modified_attributes_for_update' do 269 | before :each do 270 | model_one.save! 271 | allow(ModelOne).to receive(:dynamic_enabled?) { false } 272 | allow(model_one).to receive(:changes) { changes } 273 | end 274 | let(:changes) { {} } 275 | subject { model_one.send(:modified_attributes_for_update) } 276 | 277 | context 'when embeds_one attributes passed in options' do 278 | before :each do 279 | ModelOne.track_history(modifier_field_optional: true, on: { emb_one: :f_em_foo }) 280 | end 281 | let(:changes) { { 'emb_one' => [{ 'f_em_foo' => 'Foo', 'fmb' => 'Bar' }, { 'f_em_foo' => 'Foo-new', 'fmb' => 'Bar-new' }] } } 282 | it { expect(subject['emb_one'][0]).to eq('f_em_foo' => 'Foo') } 283 | it { expect(subject['emb_one'][1]).to eq('f_em_foo' => 'Foo-new') } 284 | end 285 | 286 | context 'when embeds_one relation passed in options' do 287 | before :each do 288 | ModelOne.track_history(modifier_field_optional: true, on: :emb_one) 289 | end 290 | let(:changes) { { 'emb_one' => [{ 'f_em_foo' => 'Foo', 'fmb' => 'Bar' }, { 'f_em_foo' => 'Foo-new', 'fmb' => 'Bar-new' }] } } 291 | it { expect(subject['emb_one'][0]).to eq('f_em_foo' => 'Foo', 'fmb' => 'Bar') } 292 | it { expect(subject['emb_one'][1]).to eq('f_em_foo' => 'Foo-new', 'fmb' => 'Bar-new') } 293 | end 294 | 295 | context 'when embeds_one relation not tracked' do 296 | before :each do 297 | ModelOne.track_history(modifier_field_optional: true, on: :fields) 298 | end 299 | let(:changes) { { 'emb_one' => [{ 'f_em_foo' => 'Foo' }, { 'f_em_foo' => 'Foo-new' }] } } 300 | it { expect(subject['emb_one']).to be_nil } 301 | end 302 | 303 | context 'when embeds_many attributes passed in options' do 304 | before :each do 305 | ModelOne.track_history(modifier_field_optional: true, on: { emb_threes: :f_em_foo }) 306 | end 307 | let(:changes) { { 'emb_threes' => [[{ 'f_em_foo' => 'Foo', 'fmb' => 'Bar' }], [{ 'f_em_foo' => 'Foo-new', 'fmb' => 'Bar-new' }]] } } 308 | it { expect(subject['emb_threes']).to eq [[{ 'f_em_foo' => 'Foo' }], [{ 'f_em_foo' => 'Foo-new' }]] } 309 | end 310 | 311 | context 'when embeds_many relation passed in options' do 312 | before :each do 313 | ModelOne.track_history(modifier_field_optional: true, on: :emb_threes) 314 | end 315 | let(:changes) { { 'emb_threes' => [[{ 'f_em_foo' => 'Foo', 'fmb' => 'Bar' }], [{ 'f_em_foo' => 'Foo-new', 'fmb' => 'Bar-new' }]] } } 316 | it { expect(subject['emb_threes']).to eq [[{ 'f_em_foo' => 'Foo', 'fmb' => 'Bar' }], [{ 'f_em_foo' => 'Foo-new', 'fmb' => 'Bar-new' }]] } 317 | end 318 | 319 | context 'when embeds_many relation not tracked' do 320 | before :each do 321 | ModelOne.track_history(modifier_field_optional: true, on: :fields) 322 | end 323 | let(:changes) { { 'emb_threes' => [[{ 'f_em_foo' => 'Foo' }], [{ 'f_em_foo' => 'Foo-new' }]] } } 324 | it { expect(subject['emb_threes']).to be_nil } 325 | end 326 | 327 | context 'when field tracked' do 328 | before :each do 329 | ModelOne.track_history(modifier_field_optional: true, on: :foo) 330 | end 331 | let(:changes) { { 'foo' => ['Foo', 'Foo-new'], 'b' => ['Bar', 'Bar-new'] } } 332 | it { is_expected.to eq('foo' => ['Foo', 'Foo-new']) } 333 | end 334 | 335 | context 'when field not tracked' do 336 | before :each do 337 | ModelOne.track_history(modifier_field_optional: true, on: []) 338 | end 339 | let(:changes) { { 'foo' => ['Foo', 'Foo-new'] } } 340 | it { is_expected.to eq({}) } 341 | end 342 | 343 | describe 'embeds_one' do 344 | before :each do 345 | class Email 346 | include Mongoid::Document 347 | include Mongoid::History::Trackable 348 | 349 | store_in collection: :emails 350 | field :provider 351 | 352 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 353 | embeds_one :email_subject 354 | else 355 | embeds_one :email_subject, inverse_class_name: 'EmailSubject' 356 | end 357 | 358 | track_history on: :email_subject 359 | end 360 | 361 | class EmailSubject 362 | include Mongoid::Document 363 | include Mongoid::History::Trackable 364 | 365 | field :content 366 | embedded_in :email_subject 367 | end 368 | end 369 | 370 | after :each do 371 | Object.send(:remove_const, :EmailSubject) 372 | Object.send(:remove_const, :Email) 373 | end 374 | 375 | before :each do 376 | allow(Email).to receive(:dynamic_enabled?) { false } 377 | allow(email).to receive(:changes) { changes } 378 | end 379 | 380 | let(:email) { Email.new } 381 | let(:changes) { {} } 382 | subject { email.send(:modified_attributes_for_update)['email_subject'] } 383 | 384 | context 'when paranoia_field not present' do 385 | let(:changes) { { 'email_subject' => [{ 'content' => 'Content' }, { 'content' => 'Content-new' }] } } 386 | it { is_expected.to eq [{ 'content' => 'Content' }, { 'content' => 'Content-new' }] } 387 | end 388 | 389 | context 'when older soft-deleted' do 390 | let(:changes) { { 'email_subject' => [{ 'content' => 'Content', 'deleted_at' => Time.now }, { 'content' => 'Content-new' }] } } 391 | it { is_expected.to eq [{}, { 'content' => 'Content-new' }] } 392 | end 393 | 394 | context 'when new soft-deleted' do 395 | let(:changes) { { 'email_subject' => [{ 'content' => 'Content' }, { 'content' => 'Content-new', 'deleted_at' => Time.now }] } } 396 | it { is_expected.to eq [{ 'content' => 'Content' }, {}] } 397 | end 398 | 399 | context 'when not soft-deleted' do 400 | let(:changes) do 401 | { 'email_subject' => [{ 'content' => 'Content', 'deleted_at' => nil }, { 'content' => 'Content-new', 'deleted_at' => nil }] } 402 | end 403 | it { is_expected.to eq [{ 'content' => 'Content' }, { 'content' => 'Content-new' }] } 404 | end 405 | end 406 | 407 | describe 'paranoia_field' do 408 | context 'when embeds_one has alias' do 409 | before :each do 410 | class ModelTwo 411 | include Mongoid::Document 412 | include Mongoid::History::Trackable 413 | 414 | store_in collection: :model_twos 415 | 416 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 417 | embeds_one :emb_two_one 418 | else 419 | embeds_one :emb_two_one, inverse_class_name: 'EmbTwoOne' 420 | end 421 | 422 | track_history on: :emb_two_one 423 | end 424 | 425 | class EmbTwoOne 426 | include Mongoid::Document 427 | include Mongoid::History::Trackable 428 | 429 | field :foo 430 | field :cncl, as: :cancelled_at 431 | 432 | embedded_in :model_two 433 | 434 | history_settings paranoia_field: :cancelled_at 435 | end 436 | end 437 | 438 | after :each do 439 | Object.send(:remove_const, :ModelTwo) 440 | Object.send(:remove_const, :EmbTwoOne) 441 | end 442 | 443 | before :each do 444 | allow(ModelTwo).to receive(:dynamic_enabled?) { false } 445 | allow(model_two_obj).to receive(:changes) { changes } 446 | end 447 | 448 | let(:model_two_obj) { ModelTwo.new } 449 | let(:changes) { { 'emb_two_one' => [{ 'foo' => 'Foo', 'cncl' => Time.now }, { 'foo' => 'Foo-new' }] } } 450 | 451 | subject { model_two_obj.send(:modified_attributes_for_update)['emb_two_one'] } 452 | it { is_expected.to eq [{}, { 'foo' => 'Foo-new' }] } 453 | end 454 | 455 | context 'when embeds_many has alias' do 456 | before :each do 457 | class ModelTwo 458 | include Mongoid::Document 459 | include Mongoid::History::Trackable 460 | 461 | store_in collection: :model_twos 462 | 463 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 464 | embeds_many :emb_two_ones 465 | else 466 | embeds_many :emb_two_ones, inverse_class_name: 'EmbTwoOne' 467 | end 468 | 469 | track_history on: :emb_two_ones 470 | end 471 | 472 | class EmbTwoOne 473 | include Mongoid::Document 474 | include Mongoid::History::Trackable 475 | 476 | field :foo 477 | field :cncl, as: :cancelled_at 478 | 479 | embedded_in :model_two 480 | 481 | history_settings paranoia_field: :cancelled_at 482 | end 483 | end 484 | 485 | after :each do 486 | Object.send(:remove_const, :ModelTwo) 487 | Object.send(:remove_const, :EmbTwoOne) 488 | end 489 | 490 | before :each do 491 | allow(ModelTwo).to receive(:dynamic_enabled?) { false } 492 | allow(model_two_obj).to receive(:changes) { changes } 493 | end 494 | 495 | let(:model_two_obj) { ModelTwo.new } 496 | let(:changes) { { 'emb_two_ones' => [[{ 'foo' => 'Foo', 'cncl' => Time.now }], [{ 'foo' => 'Foo-new' }]] } } 497 | subject { model_two_obj.send(:modified_attributes_for_update)['emb_two_ones'] } 498 | it { is_expected.to eq [[], [{ 'foo' => 'Foo-new' }]] } 499 | end 500 | end 501 | end 502 | 503 | describe '#modified_attributes_for_destroy' do 504 | before :each do 505 | allow(ModelOne).to receive(:dynamic_enabled?) { false } 506 | model_one.save! 507 | end 508 | subject { model_one.send(:modified_attributes_for_destroy) } 509 | 510 | context 'with tracked embeds_one object' do 511 | before :each do 512 | ModelOne.track_history(modifier_field_optional: true, on: { emb_one: :f_em_foo }) 513 | end 514 | it 'should include tracked attributes only' do 515 | expect(subject['emb_one'][0].keys.size).to eq 2 516 | expect(subject['emb_one'][0]['_id']).to eq emb_one._id 517 | expect(subject['emb_one'][0]['f_em_foo']).to eq 'Foo' 518 | 519 | expect(subject['emb_one'][1]).to be_nil 520 | end 521 | end 522 | 523 | context 'with untracked embeds_one object' do 524 | before :each do 525 | ModelOne.track_history(modifier_field_optional: true, on: :fields) 526 | end 527 | it 'should not include embeds_one attributes' do 528 | expect(subject['emb_one']).to be_nil 529 | end 530 | end 531 | 532 | context 'with tracked embeds_many objects' do 533 | before :each do 534 | ModelOne.track_history(modifier_field_optional: true, on: { emb_threes: :f_em_foo }) 535 | end 536 | it 'should include tracked attributes only' do 537 | expect(subject['emb_threes'][0][0].keys.count).to eq 2 538 | expect(subject['emb_threes'][0][0]['_id']).to eq emb_threes.first._id 539 | expect(subject['emb_threes'][0][0]['f_em_foo']).to eq 'Foo' 540 | 541 | expect(subject['emb_threes'][1]).to be_nil 542 | end 543 | end 544 | 545 | context 'with untracked embeds_many objects' do 546 | before :each do 547 | ModelOne.track_history(modifier_field_optional: true, on: :fields) 548 | end 549 | it 'should include not tracked embeds_many attributes' do 550 | expect(subject['emb_threes']).to be_nil 551 | end 552 | end 553 | end 554 | end 555 | end 556 | -------------------------------------------------------------------------------- /spec/unit/options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Options do 4 | before :each do 5 | class ModelOne 6 | include Mongoid::Document 7 | include Mongoid::History::Trackable 8 | 9 | store_in collection: :model_ones 10 | 11 | field :foo 12 | field :b, as: :bar 13 | 14 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 15 | embeds_one :emb_one 16 | embeds_one :emb_two, store_as: :emtw 17 | embeds_many :emb_threes 18 | embeds_many :emb_fours, store_as: :emfs 19 | else 20 | embeds_one :emb_one, inverse_class_name: 'EmbOne' 21 | embeds_one :emb_two, store_as: :emtw, inverse_class_name: 'EmbTwo' 22 | embeds_many :emb_threes, inverse_class_name: 'EmbThree' 23 | embeds_many :emb_fours, store_as: :emfs, inverse_class_name: 'EmbFour' 24 | end 25 | 26 | track_history 27 | end 28 | 29 | class EmbOne 30 | include Mongoid::Document 31 | 32 | field :f_em_foo 33 | field :fmb, as: :f_em_bar 34 | 35 | embedded_in :model_one 36 | end 37 | 38 | class EmbTwo 39 | include Mongoid::Document 40 | 41 | field :f_em_baz 42 | embedded_in :model_one 43 | end 44 | 45 | class EmbThree 46 | include Mongoid::Document 47 | 48 | field :f_em_foo 49 | field :fmb, as: :f_em_bar 50 | 51 | embedded_in :model_one 52 | end 53 | 54 | class EmbFour 55 | include Mongoid::Document 56 | 57 | field :f_em_baz 58 | embedded_in :model_one 59 | end 60 | end 61 | 62 | after :each do 63 | Object.send(:remove_const, :ModelOne) 64 | Object.send(:remove_const, :EmbOne) 65 | Object.send(:remove_const, :EmbTwo) 66 | Object.send(:remove_const, :EmbThree) 67 | Object.send(:remove_const, :EmbFour) 68 | end 69 | 70 | let(:options) { {} } 71 | let(:service) { described_class.new(ModelOne, options) } 72 | 73 | subject { service } 74 | 75 | it { is_expected.to respond_to :trackable } 76 | it { is_expected.to respond_to :options } 77 | 78 | describe '#initialize' do 79 | it { expect(service.trackable).to eq ModelOne } 80 | end 81 | 82 | describe '#scope' do 83 | it { expect(service.scope).to eq :model_one } 84 | end 85 | 86 | describe '#parse' do 87 | it 'does not mutate the original options' do 88 | original_options = service.options.dup 89 | service.prepared 90 | expect(service.options).to eq original_options 91 | end 92 | 93 | describe '#default_options' do 94 | let(:expected_options) do 95 | { 96 | on: :all, 97 | except: %i[created_at updated_at], 98 | tracker_class_name: nil, 99 | modifier_field: :modifier, 100 | version_field: :version, 101 | changes_method: :changes, 102 | scope: :model_one, 103 | track_create: true, 104 | track_update: true, 105 | track_destroy: true, 106 | track_blank_changes: false, 107 | format: nil 108 | } 109 | end 110 | it { expect(service.send(:default_options)).to eq expected_options } 111 | end 112 | 113 | describe '#prepare_skipped_fields' do 114 | let(:options) { { except: value } } 115 | subject { service.prepared } 116 | 117 | context 'with field' do 118 | let(:value) { :foo } 119 | it { expect(subject[:except]).to eq %w[foo] } 120 | end 121 | 122 | context 'with array of fields' do 123 | let(:value) { %i[foo] } 124 | it { expect(subject[:except]).to eq %w[foo] } 125 | end 126 | 127 | context 'with field alias' do 128 | let(:value) { %i[foo bar] } 129 | it { expect(subject[:except]).to eq %w[foo b] } 130 | end 131 | 132 | context 'with duplicate values' do 133 | let(:value) { %i[foo bar b] } 134 | it { expect(subject[:except]).to eq %w[foo b] } 135 | end 136 | 137 | context 'with blank values' do 138 | let(:value) { %i[foo] | [nil] } 139 | it { expect(subject[:except]).to eq %w[foo] } 140 | end 141 | end 142 | 143 | describe '#prepare_formatted_fields' do 144 | let(:options) { { format: value } } 145 | subject { service.prepared } 146 | 147 | context 'with non-hash' do 148 | let(:value) { :foo } 149 | it { expect(subject[:format]).to eq({}) } 150 | end 151 | 152 | context 'with a field format' do 153 | let(:value) { { foo: '&&&' } } 154 | it { expect(subject[:format]).to include 'foo' => '&&&' } 155 | end 156 | 157 | context 'with nested format' do 158 | let(:value) { { emb_one: { f_em_foo: '***' } } } 159 | it { expect(subject[:format]).to include 'emb_one' => { 'f_em_foo' => '***' } } 160 | end 161 | end 162 | 163 | describe '#parse_tracked_fields_and_relations' do 164 | context 'when options not passed' do 165 | let(:expected_options) do 166 | { 167 | on: %i[foo b], 168 | except: %w[created_at updated_at], 169 | tracker_class_name: nil, 170 | modifier_field: :modifier, 171 | version_field: :version, 172 | changes_method: :changes, 173 | scope: :model_one, 174 | track_create: true, 175 | track_update: true, 176 | track_destroy: true, 177 | track_blank_changes: false, 178 | fields: %w[foo b], 179 | dynamic: [], 180 | relations: { embeds_one: {}, embeds_many: {} }, 181 | format: {} 182 | } 183 | end 184 | it { expect(service.prepared).to eq expected_options } 185 | end 186 | 187 | context 'when options passed' do 188 | subject { service.prepared } 189 | 190 | describe '@options' do 191 | let(:options) { { on: value } } 192 | 193 | context 'with field' do 194 | let(:value) { :foo } 195 | it { expect(subject[:on]).to eq %i[foo] } 196 | it { expect(subject[:fields]).to eq %w[foo] } 197 | end 198 | 199 | context 'with array of fields' do 200 | let(:value) { %i[foo] } 201 | it { expect(subject[:on]).to eq %i[foo] } 202 | it { expect(subject[:fields]).to eq %w[foo] } 203 | end 204 | 205 | context 'with embeds_one relation attributes' do 206 | let(:value) { { emb_one: %i[f_em_foo] } } 207 | it { expect(subject[:on]).to eq [[:emb_one, %i[f_em_foo]]] } 208 | end 209 | 210 | context 'with fields and embeds_one relation attributes' do 211 | let(:value) { [:foo, emb_one: %i[f_em_foo]] } 212 | it { expect(subject[:on]).to eq [:foo, emb_one: %i[f_em_foo]] } 213 | end 214 | 215 | context 'with :all' do 216 | let(:value) { :all } 217 | it { expect(subject[:on]).to eq %i[foo b] } 218 | end 219 | 220 | context 'with :fields' do 221 | let(:value) { :fields } 222 | it { expect(subject[:on]).to eq %i[foo b] } 223 | end 224 | 225 | describe '#categorize_tracked_option' do 226 | context 'with skipped field' do 227 | let(:options) { { on: %i[foo bar], except: :foo } } 228 | it { expect(subject[:fields]).to eq %w[b] } 229 | end 230 | 231 | context 'with skipped embeds_one relation' do 232 | let(:options) { { on: %i[fields emb_one emb_two], except: :emb_one } } 233 | it { expect(subject[:relations][:embeds_one]).to eq('emtw' => %w[_id f_em_baz]) } 234 | end 235 | 236 | context 'with skipped embeds_many relation' do 237 | let(:options) { { on: %i[fields emb_threes emb_fours], except: :emb_threes } } 238 | it { expect(subject[:relations][:embeds_many]).to eq('emfs' => %w[_id f_em_baz]) } 239 | end 240 | 241 | context 'with reserved field' do 242 | let(:options) { { on: %i[_id _type foo deleted_at] } } 243 | it { expect(subject[:fields]).to eq %w[foo] } 244 | end 245 | 246 | context 'when embeds_one attribute passed' do 247 | let(:options) { { on: { emb_one: :f_em_foo } } } 248 | it { expect(subject[:relations][:embeds_one]).to eq('emb_one' => %w[_id f_em_foo]) } 249 | end 250 | 251 | context 'when embeds_one attributes array passed' do 252 | let(:options) { { on: { emb_one: %i[f_em_foo] } } } 253 | it { expect(subject[:relations][:embeds_one]).to eq('emb_one' => %w[_id f_em_foo]) } 254 | end 255 | 256 | context 'when embeds_many attribute passed' do 257 | let(:options) { { on: { emb_threes: :f_em_foo } } } 258 | it { expect(subject[:relations][:embeds_many]).to eq('emb_threes' => %w[_id f_em_foo]) } 259 | end 260 | 261 | context 'when embeds_many attributes array passed' do 262 | let(:options) { { on: { emb_threes: %i[f_em_foo] } } } 263 | it { expect(subject[:relations][:embeds_many]).to eq('emb_threes' => %w[_id f_em_foo]) } 264 | end 265 | 266 | context 'when embeds_one attributes not passed' do 267 | let(:options) { { on: :emb_one } } 268 | it { expect(subject[:relations][:embeds_one]).to eq('emb_one' => %w[_id f_em_foo fmb]) } 269 | end 270 | 271 | context 'when embeds_many attributes not passed' do 272 | let(:options) { { on: :emb_threes } } 273 | it { expect(subject[:relations][:embeds_many]).to eq('emb_threes' => %w[_id f_em_foo fmb]) } 274 | end 275 | 276 | context 'when embeds_one attribute alias passed' do 277 | let(:options) { { on: { emb_one: %i[f_em_bar] } } } 278 | it { expect(subject[:relations][:embeds_one]).to eq('emb_one' => %w[_id fmb]) } 279 | end 280 | 281 | context 'when embeds_many attribute alias passed' do 282 | let(:options) { { on: { emb_threes: %i[f_em_bar] } } } 283 | it { expect(subject[:relations][:embeds_many]).to eq('emb_threes' => %w[_id fmb]) } 284 | end 285 | 286 | context 'with fields, and multiple embeds_one, and embeds_many relations' do 287 | let(:options) { { on: [:foo, :bar, :emb_two, { emb_threes: %i[f_em_foo f_em_bar], emb_fours: :f_em_baz }] } } 288 | it 'should categorize fields and associations correctly' do 289 | expect(subject[:fields]).to eq(%w[foo b]) 290 | expect(subject[:relations][:embeds_one]).to eq('emtw' => %w[_id f_em_baz]) 291 | expect(subject[:relations][:embeds_many]).to eq('emb_threes' => %w[_id f_em_foo fmb], 'emfs' => %w[_id f_em_baz]) 292 | end 293 | end 294 | 295 | context 'with field alias' do 296 | let(:options) { { on: :bar } } 297 | it { expect(subject[:fields]).to eq %w[b] } 298 | end 299 | 300 | context 'with dynamic field name' do 301 | let(:options) { { on: :my_field } } 302 | it { expect(subject[:dynamic]).to eq %w[my_field] } 303 | end 304 | 305 | context 'with relations' do 306 | let(:options) { { on: :embedded_relations } } 307 | it do 308 | expect(subject[:relations]).to eq( 309 | embeds_many: { 'emb_threes' => %w[_id f_em_foo fmb], 310 | 'emfs' => %w[_id f_em_baz] }, 311 | embeds_one: { 'emb_one' => %w[_id f_em_foo fmb], 312 | 'emtw' => %w[_id f_em_baz] } 313 | ) 314 | end 315 | end 316 | end 317 | end 318 | 319 | describe ':modifier_field' do 320 | let(:options) { { modifier_field: :my_modifier_field } } 321 | it { expect(subject[:modifier_field]).to eq :my_modifier_field } 322 | end 323 | 324 | describe ':version_field' do 325 | let(:options) { { version_field: :my_version_field } } 326 | it { expect(subject[:version_field]).to eq :my_version_field } 327 | end 328 | 329 | describe ':paranoia_field' do 330 | let(:options) { { paranoia_field: :my_paranoia_field } } 331 | it { expect(subject[:paranoia_field]).to eq :my_paranoia_field } 332 | end 333 | 334 | describe ':changes_method' do 335 | let(:options) { { changes_method: :my_changes_method } } 336 | it { expect(subject[:changes_method]).to eq :my_changes_method } 337 | end 338 | 339 | describe ':scope' do 340 | let(:options) { { scope: :my_scope } } 341 | it { expect(subject[:scope]).to eq :my_scope } 342 | end 343 | 344 | describe ':track_create' do 345 | let(:options) { { track_create: true } } 346 | it { expect(subject[:track_create]).to be true } 347 | end 348 | 349 | describe ':track_update' do 350 | let(:options) { { track_update: false } } 351 | it { expect(subject[:track_update]).to be false } 352 | end 353 | 354 | describe ':track_destroy' do 355 | let(:options) { { track_destroy: true } } 356 | it { expect(subject[:track_destroy]).to be true } 357 | end 358 | 359 | describe ':track_blank_changes' do 360 | let(:options) { { track_blank_changes: true } } 361 | it { expect(subject[:track_blank_changes]).to be true } 362 | end 363 | 364 | describe '#remove_reserved_fields' do 365 | let(:options) { { on: %i[_id _type foo version modifier_id] } } 366 | it { expect(subject[:fields]).to eq %w[foo] } 367 | it { expect(subject[:dynamic]).to eq [] } 368 | end 369 | end 370 | end 371 | end 372 | end 373 | -------------------------------------------------------------------------------- /spec/unit/singleton_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Trackable do 4 | describe 'SingletonMethods' do 5 | before :each do 6 | class MyTrackableModel 7 | include Mongoid::Document 8 | include Mongoid::History::Trackable 9 | 10 | field :foo 11 | field :b, as: :bar 12 | 13 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 14 | embeds_one :my_embed_one_model 15 | embeds_one :my_untracked_embed_one_model 16 | embeds_many :my_embed_many_models 17 | else 18 | embeds_one :my_embed_one_model, inverse_class_name: 'MyEmbedOneModel' 19 | embeds_one :my_untracked_embed_one_model, inverse_class_name: 'MyUntrackedEmbedOneModel' 20 | embeds_many :my_embed_many_models, inverse_class_name: 'MyEmbedManyModel' 21 | end 22 | 23 | track_history on: %i[foo my_embed_one_model my_embed_many_models my_dynamic_field] 24 | end 25 | 26 | class MyEmbedOneModel 27 | include Mongoid::Document 28 | 29 | field :baz 30 | embedded_in :my_trackable_model 31 | end 32 | 33 | class MyUntrackedEmbedOneModel 34 | include Mongoid::Document 35 | 36 | field :baz 37 | embedded_in :my_trackable_model 38 | end 39 | 40 | class MyEmbedManyModel 41 | include Mongoid::Document 42 | 43 | field :bla 44 | embedded_in :my_trackable_model 45 | end 46 | end 47 | 48 | after :each do 49 | Object.send(:remove_const, :MyTrackableModel) 50 | Object.send(:remove_const, :MyEmbedOneModel) 51 | Object.send(:remove_const, :MyUntrackedEmbedOneModel) 52 | Object.send(:remove_const, :MyEmbedManyModel) 53 | end 54 | 55 | describe '#tracked?' do 56 | before { allow(MyTrackableModel).to receive(:dynamic_enabled?) { false } } 57 | it { expect(MyTrackableModel.tracked?(:foo)).to be true } 58 | it { expect(MyTrackableModel.tracked?(:bar)).to be false } 59 | it { expect(MyTrackableModel.tracked?(:my_embed_one_model)).to be true } 60 | it { expect(MyTrackableModel.tracked?(:my_untracked_embed_one_model)).to be false } 61 | it { expect(MyTrackableModel.tracked?(:my_embed_many_models)).to be true } 62 | it { expect(MyTrackableModel.tracked?(:my_dynamic_field)).to be true } 63 | end 64 | 65 | describe '#dynamic_field?' do 66 | before :each do 67 | class EmbOne 68 | include Mongoid::Document 69 | 70 | embedded_in :my_model 71 | end 72 | end 73 | 74 | after :each do 75 | Object.send(:remove_const, :EmbOne) 76 | end 77 | 78 | context 'when dynamic enabled' do 79 | context 'with embeds one relation' do 80 | before :each do 81 | class MyModel 82 | include Mongoid::Document 83 | include Mongoid::History::Trackable 84 | 85 | store_in collection: :my_models 86 | 87 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 88 | embeds_one :emb_one 89 | else 90 | embeds_one :emb_one, inverse_class_name: 'EmbOne' 91 | end 92 | 93 | track_history 94 | end 95 | end 96 | 97 | after :each do 98 | Object.send(:remove_const, :MyModel) 99 | end 100 | 101 | it 'should track dynamic field' do 102 | allow(MyModel).to receive(:dynamic_enabled?) { true } 103 | expect(MyModel.dynamic_field?(:foo)).to be true 104 | end 105 | 106 | it 'should not track embeds_one relation' do 107 | allow(MyModel).to receive(:dynamic_enabled?) { true } 108 | expect(MyModel.dynamic_field?(:emb_one)).to be false 109 | end 110 | end 111 | 112 | context 'with embeds one relation and alias' do 113 | before :each do 114 | class MyModel 115 | include Mongoid::Document 116 | include Mongoid::History::Trackable 117 | 118 | store_in collection: :my_models 119 | 120 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 121 | embeds_one :emb_one, store_as: :emo 122 | else 123 | embeds_one :emb_one, inverse_class_name: 'EmbOne', store_as: :emo 124 | end 125 | 126 | track_history 127 | end 128 | end 129 | 130 | after :each do 131 | Object.send(:remove_const, :MyModel) 132 | end 133 | 134 | it 'should not track embeds_one relation' do 135 | allow(MyModel).to receive(:dynamic_enabled?) { true } 136 | expect(MyModel.dynamic_field?(:emo)).to be false 137 | end 138 | end 139 | 140 | context 'with embeds many relation' do 141 | before :each do 142 | class MyModel 143 | include Mongoid::Document 144 | include Mongoid::History::Trackable 145 | 146 | store_in collection: :my_models 147 | 148 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 149 | embeds_many :emb_ones 150 | else 151 | embeds_many :emb_ones, inverse_class_name: 'EmbOne' 152 | end 153 | 154 | track_history 155 | end 156 | end 157 | 158 | after :each do 159 | Object.send(:remove_const, :MyModel) 160 | end 161 | 162 | it 'should not track embeds_many relation' do 163 | allow(MyModel).to receive(:dynamic_enabled?) { true } 164 | expect(MyModel.dynamic_field?(:emb_ones)).to be false 165 | end 166 | end 167 | 168 | context 'with embeds many relation and alias' do 169 | before :each do 170 | class MyModel 171 | include Mongoid::Document 172 | include Mongoid::History::Trackable 173 | 174 | store_in collection: :my_models 175 | 176 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 177 | embeds_many :emb_ones, store_as: :emos 178 | else 179 | embeds_many :emb_ones, store_as: :emos, inverse_class_name: 'EmbOne' 180 | end 181 | track_history 182 | end 183 | end 184 | 185 | after :each do 186 | Object.send(:remove_const, :MyModel) 187 | end 188 | 189 | it 'should not track embeds_many relation' do 190 | allow(MyModel).to receive(:dynamic_enabled?) { true } 191 | expect(MyModel.dynamic_field?(:emos)).to be false 192 | end 193 | end 194 | end 195 | end 196 | 197 | describe '#tracked_fields' do 198 | it 'should include fields and dynamic fields' do 199 | expect(MyTrackableModel.tracked_fields).to eq %w[foo my_dynamic_field] 200 | end 201 | end 202 | 203 | describe '#tracked_relation?' do 204 | it 'should return true if a relation is tracked' do 205 | expect(MyTrackableModel.tracked_relation?(:my_embed_one_model)).to be true 206 | expect(MyTrackableModel.tracked_relation?(:my_untracked_embed_one_model)).to be false 207 | expect(MyTrackableModel.tracked_relation?(:my_embed_many_models)).to be true 208 | end 209 | end 210 | 211 | describe '#tracked_embeds_one?' do 212 | it { expect(MyTrackableModel.tracked_embeds_one?(:my_embed_one_model)).to be true } 213 | it { expect(MyTrackableModel.tracked_embeds_one?(:my_untracked_embed_one_model)).to be false } 214 | it { expect(MyTrackableModel.tracked_embeds_one?(:my_embed_many_models)).to be false } 215 | end 216 | 217 | describe '#tracked_embeds_one' do 218 | it { expect(MyTrackableModel.tracked_embeds_one).to include 'my_embed_one_model' } 219 | it { expect(MyTrackableModel.tracked_embeds_one).to_not include 'my_untracked_embed_one_model' } 220 | end 221 | 222 | describe '#tracked_embeds_one_attributes' do 223 | before :each do 224 | class ModelOne 225 | include Mongoid::Document 226 | include Mongoid::History::Trackable 227 | 228 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 229 | embeds_one :emb_one 230 | embeds_one :emb_two, store_as: :emt 231 | embeds_one :emb_three 232 | else 233 | embeds_one :emb_one, inverse_class_name: 'EmbOne' 234 | embeds_one :emb_two, store_as: :emt, inverse_class_name: 'EmbTwo' 235 | embeds_one :emb_three, inverse_class_name: 'EmbThree' 236 | end 237 | end 238 | 239 | class EmbOne 240 | include Mongoid::Document 241 | 242 | field :em_foo 243 | field :em_bar 244 | 245 | embedded_in :model_one 246 | end 247 | 248 | class EmbTwo 249 | include Mongoid::Document 250 | 251 | field :em_bar 252 | embedded_in :model_one 253 | end 254 | 255 | class EmbThree 256 | include Mongoid::Document 257 | 258 | field :em_baz 259 | embedded_in :model_one 260 | end 261 | end 262 | 263 | after :each do 264 | Object.send(:remove_const, :ModelOne) 265 | Object.send(:remove_const, :EmbOne) 266 | Object.send(:remove_const, :EmbTwo) 267 | Object.send(:remove_const, :EmbThree) 268 | end 269 | 270 | context 'when relation tracked' do 271 | before(:each) { ModelOne.track_history(on: :emb_one) } 272 | it { expect(ModelOne.tracked_embeds_one_attributes('emb_one')).to eq %w[_id em_foo em_bar] } 273 | end 274 | 275 | context 'when relation tracked with alias' do 276 | before(:each) { ModelOne.track_history(on: :emb_two) } 277 | it { expect(ModelOne.tracked_embeds_one_attributes('emb_two')).to eq %w[_id em_bar] } 278 | end 279 | 280 | context 'when relation tracked with attributes' do 281 | before(:each) { ModelOne.track_history(on: { emb_one: :em_foo }) } 282 | it { expect(ModelOne.tracked_embeds_one_attributes('emb_one')).to eq %w[_id em_foo] } 283 | end 284 | 285 | context 'when relation not tracked' do 286 | before(:each) { ModelOne.track_history(on: :fields) } 287 | it { expect(ModelOne.tracked_embeds_one_attributes('emb_one')).to be_nil } 288 | end 289 | end 290 | 291 | describe '#tracked_embeds_many?' do 292 | it { expect(MyTrackableModel.tracked_embeds_many?(:my_embed_one_model)).to be false } 293 | it { expect(MyTrackableModel.tracked_embeds_many?(:my_untracked_embed_one_model)).to be false } 294 | it { expect(MyTrackableModel.tracked_embeds_many?(:my_embed_many_models)).to be true } 295 | end 296 | 297 | describe '#tracked_embeds_many' do 298 | it { expect(MyTrackableModel.tracked_embeds_many).to eq ['my_embed_many_models'] } 299 | end 300 | 301 | describe '#tracked_embeds_many_attributes' do 302 | before :each do 303 | class ModelOne 304 | include Mongoid::Document 305 | include Mongoid::History::Trackable 306 | 307 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 308 | embeds_many :emb_ones 309 | embeds_many :emb_twos, store_as: :emts 310 | embeds_many :emb_threes 311 | else 312 | embeds_many :emb_ones, inverse_class_name: 'EmbOne' 313 | embeds_many :emb_twos, store_as: :emts, inverse_class_name: 'EmbTwo' 314 | embeds_many :emb_threes, inverse_class_name: 'EmbThree' 315 | end 316 | end 317 | 318 | class EmbOne 319 | include Mongoid::Document 320 | 321 | field :em_foo 322 | field :em_bar 323 | 324 | embedded_in :model_one 325 | end 326 | 327 | class EmbTwo 328 | include Mongoid::Document 329 | 330 | field :em_bar 331 | embedded_in :model_one 332 | end 333 | 334 | class EmbThree 335 | include Mongoid::Document 336 | 337 | field :em_baz 338 | embedded_in :model_one 339 | end 340 | end 341 | 342 | after :each do 343 | Object.send(:remove_const, :ModelOne) 344 | Object.send(:remove_const, :EmbOne) 345 | Object.send(:remove_const, :EmbTwo) 346 | Object.send(:remove_const, :EmbThree) 347 | end 348 | 349 | context 'when relation tracked' do 350 | before(:each) { ModelOne.track_history(on: :emb_ones) } 351 | it { expect(ModelOne.tracked_embeds_many_attributes('emb_ones')).to eq %w[_id em_foo em_bar] } 352 | end 353 | 354 | context 'when relation tracked with alias' do 355 | before(:each) { ModelOne.track_history(on: :emb_twos) } 356 | it { expect(ModelOne.tracked_embeds_many_attributes('emb_twos')).to eq %w[_id em_bar] } 357 | end 358 | 359 | context 'when relation tracked with attributes' do 360 | before(:each) { ModelOne.track_history(on: { emb_ones: :em_foo }) } 361 | it { expect(ModelOne.tracked_embeds_many_attributes('emb_ones')).to eq %w[_id em_foo] } 362 | end 363 | 364 | context 'when relation not tracked' do 365 | before(:each) { ModelOne.track_history(on: :fields) } 366 | it { expect(ModelOne.tracked_embeds_many_attributes('emb_ones')).to be_nil } 367 | end 368 | end 369 | 370 | describe '#trackable_scope' do 371 | before :each do 372 | class ModelOne 373 | include Mongoid::Document 374 | include Mongoid::History::Trackable 375 | 376 | store_in collection: :model_ones 377 | 378 | track_history 379 | end 380 | end 381 | 382 | it { expect(ModelOne.trackable_scope).to eq(:model_one) } 383 | end 384 | 385 | describe '#clear_trackable_memoization' do 386 | before :each do 387 | MyTrackableModel.instance_variable_set(:@reserved_tracked_fields, %w[_id _type]) 388 | MyTrackableModel.instance_variable_set(:@history_trackable_options, on: %w[fields]) 389 | MyTrackableModel.instance_variable_set(:@trackable_settings, paranoia_field: 'deleted_at') 390 | MyTrackableModel.instance_variable_set(:@tracked_fields, %w[foo]) 391 | MyTrackableModel.instance_variable_set(:@tracked_embeds_one, %w[my_embed_one_model]) 392 | MyTrackableModel.instance_variable_set(:@tracked_embeds_many, %w[my_embed_many_models]) 393 | MyTrackableModel.clear_trackable_memoization 394 | end 395 | 396 | it 'should clear all the trackable memoization' do 397 | expect(MyTrackableModel.instance_variable_get(:@reserved_tracked_fields)).to be_nil 398 | expect(MyTrackableModel.instance_variable_get(:@history_trackable_options)).to be_nil 399 | expect(MyTrackableModel.instance_variable_get(:@trackable_settings)).to be_nil 400 | expect(MyTrackableModel.instance_variable_get(:@tracked_fields)).to be_nil 401 | expect(MyTrackableModel.instance_variable_get(:@tracked_embeds_one)).to be_nil 402 | expect(MyTrackableModel.instance_variable_get(:@tracked_embeds_many)).to be_nil 403 | end 404 | end 405 | end 406 | end 407 | -------------------------------------------------------------------------------- /spec/unit/store/default_store_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Default Store' do 4 | describe 'Mongoid::History' do 5 | describe '.store' do 6 | it 'should return Thread object' do 7 | expect(Mongoid::History.store).to be_a Thread 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/unit/store/request_store_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'RequestStore' do 4 | before { stub_const('RequestStore', RequestStoreTemp) } 5 | 6 | describe 'Mongoid::History' do 7 | describe '.store' do 8 | it 'should return RequestStore' do 9 | expect(Mongoid::History.store).to be_a Hash 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/unit/tracker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::History::Tracker do 4 | context 'when included' do 5 | before :each do 6 | Mongoid::History.tracker_class_name = nil 7 | 8 | class MyTracker 9 | include Mongoid::History::Tracker 10 | end 11 | end 12 | 13 | after :each do 14 | Object.send(:remove_const, :MyTracker) 15 | end 16 | 17 | it 'should set tracker_class_name when included' do 18 | expect(Mongoid::History.tracker_class_name).to eq(:my_tracker) 19 | end 20 | 21 | it 'should set fields defaults' do 22 | expect(MyTracker.new.association_chain).to eq([]) 23 | expect(MyTracker.new.original).to eq({}) 24 | expect(MyTracker.new.modified).to eq({}) 25 | end 26 | end 27 | 28 | describe '#tracked_edits' do 29 | before :each do 30 | class TrackerOne 31 | include Mongoid::History::Tracker 32 | end 33 | 34 | class ModelOne 35 | include Mongoid::Document 36 | include Mongoid::History::Trackable 37 | 38 | store_in collection: :model_ones 39 | 40 | if Mongoid::Compatibility::Version.mongoid7_or_newer? 41 | embeds_many :emb_ones 42 | else 43 | embeds_many :emb_ones, inverse_class_name: 'EmbOne' 44 | end 45 | end 46 | 47 | class EmbOne 48 | include Mongoid::Document 49 | 50 | field :em_foo 51 | embedded_in :model_one 52 | end 53 | end 54 | 55 | after :each do 56 | Object.send(:remove_const, :TrackerOne) 57 | Object.send(:remove_const, :ModelOne) 58 | Object.send(:remove_const, :EmbOne) 59 | end 60 | 61 | context 'when embeds_many' do 62 | before :each do 63 | ModelOne.track_history(on: :emb_ones) 64 | allow(tracker).to receive(:trackable_parent_class) { ModelOne } 65 | end 66 | 67 | let(:tracker) { TrackerOne.new } 68 | 69 | describe '#prepare_tracked_edits_for_embeds_many' do 70 | before :each do 71 | allow(tracker).to receive(:tracked_changes) { changes } 72 | end 73 | 74 | let(:emb_one) { EmbOne.new } 75 | let(:emb_one_2) { EmbOne.new } 76 | let(:emb_one_3) { EmbOne.new } 77 | let(:changes) { {} } 78 | 79 | subject { tracker.tracked_edits['embeds_many']['emb_ones'] } 80 | 81 | context 'when all values present' do 82 | let(:changes) do 83 | { 84 | 'emb_ones' => { 85 | from: [{ '_id' => emb_one._id, 'em_foo' => 'Em-Foo' }, 86 | { '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2' }], 87 | to: [{ '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2-new' }, 88 | { '_id' => emb_one_3._id, 'em_foo' => 'Em-Foo-3' }] 89 | } 90 | } 91 | end 92 | 93 | it 'should include :add, :remove, and :modify' do 94 | expect(subject['add']).to eq [{ '_id' => emb_one_3._id, 'em_foo' => 'Em-Foo-3' }] 95 | expect(subject['remove']).to eq [{ '_id' => emb_one._id, 'em_foo' => 'Em-Foo' }] 96 | expect(subject['modify'].size).to eq 1 97 | expect(subject['modify'][0]['from']).to eq('_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2') 98 | expect(subject['modify'][0]['to']).to eq('_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2-new') 99 | end 100 | end 101 | 102 | context 'when value :from blank' do 103 | let(:changes) do 104 | { 105 | 'emb_ones' => { 106 | to: [{ '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2-new' }, 107 | { '_id' => emb_one_3._id, 'em_foo' => 'Em-Foo-3' }] 108 | } 109 | } 110 | end 111 | it 'should include :add' do 112 | expect(subject['add'].size).to eq 2 113 | expect(subject['add'][0]).to eq('_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2-new') 114 | expect(subject['add'][1]).to eq('_id' => emb_one_3._id, 'em_foo' => 'Em-Foo-3') 115 | expect(subject['remove']).to be_nil 116 | expect(subject['modify']).to be_nil 117 | end 118 | end 119 | 120 | context 'when value :to blank' do 121 | let(:changes) do 122 | { 123 | 'emb_ones' => { 124 | from: [{ '_id' => emb_one._id, 'em_foo' => 'Em-Foo' }, 125 | { '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2' }] 126 | } 127 | } 128 | end 129 | it 'should include :remove' do 130 | expect(subject['add']).to be_nil 131 | expect(subject['modify']).to be_nil 132 | expect(subject['remove'].size).to eq 2 133 | expect(subject['remove'][0]).to eq('_id' => emb_one._id, 'em_foo' => 'Em-Foo') 134 | expect(subject['remove'][1]).to eq('_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2') 135 | end 136 | end 137 | 138 | context 'when no id common in :from and :to' do 139 | let(:changes) do 140 | { 141 | 'emb_ones' => { 142 | from: [{ '_id' => emb_one._id, 'em_foo' => 'Em-Foo' }], 143 | to: [{ '_id' => emb_one_3._id, 'em_foo' => 'Em-Foo-3' }] 144 | } 145 | } 146 | end 147 | it 'should include :add, and :remove' do 148 | expect(subject['add']).to eq [{ '_id' => emb_one_3._id, 'em_foo' => 'Em-Foo-3' }] 149 | expect(subject['modify']).to be_nil 150 | expect(subject['remove']).to eq [{ '_id' => emb_one._id, 'em_foo' => 'Em-Foo' }] 151 | end 152 | end 153 | 154 | context 'when _id attribute not set' do 155 | let(:changes) do 156 | { 157 | 'emb_ones' => { 158 | from: [{ 'em_foo' => 'Em-Foo' }, 159 | { '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2' }], 160 | to: [{ 'em_foo' => 'Em-Foo-2-new' }, 161 | { 'em_foo' => 'Em-Foo-3' }] 162 | } 163 | } 164 | end 165 | it 'should include :add, and :remove' do 166 | expect(subject['add']).to eq([{ 'em_foo' => 'Em-Foo-2-new' }, { 'em_foo' => 'Em-Foo-3' }]) 167 | expect(subject['modify']).to be_nil 168 | expect(subject['remove']).to eq [{ 'em_foo' => 'Em-Foo' }, { '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2' }] 169 | end 170 | end 171 | 172 | context 'when no change in an object' do 173 | let(:changes) do 174 | { 175 | 'emb_ones' => { 176 | from: [{ '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2' }], 177 | to: [{ '_id' => emb_one_2._id, 'em_foo' => 'Em-Foo-2' }] 178 | } 179 | } 180 | end 181 | it 'should include not :add, :remove, and :modify' do 182 | expect(subject['add']).to be_nil 183 | expect(subject['modify']).to be_nil 184 | expect(subject['remove']).to be_nil 185 | end 186 | end 187 | end 188 | end 189 | end 190 | end 191 | --------------------------------------------------------------------------------