New Upstream Release - ruby-paper-trail
Ready changes
Summary
Merged new upstream version: 14.0.0 (was: 12.0.0).
Resulting package
Built on 2023-01-20T06:34 (took 3m16s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases ruby-paper-trail
Lintian Result
Diff
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index d15c698..960cfc9 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -48,29 +48,18 @@ Testing is a little awkward because the test suite:
1. Contains a "dummy" rails app with three databases (test, foo, and bar)
1. Supports three different RDBMS': sqlite, mysql, and postgres
-### Test sqlite, AR 6
+### Test
-```
-DB=sqlite bundle exec appraisal rails-6.0 rake
-```
-
-### Test sqlite, AR 5
-
-```
-DB=sqlite bundle exec appraisal rails-5.2 rake
-```
-
-### Test mysql, AR 5
-
-```
-DB=mysql bundle exec appraisal rails-5.2 rake
-```
-
-### Test postgres, AR 5
+For most development, testing with sqlite only is easiest and sufficient. CI
+will run the rest.
```
+DB=sqlite bundle exec appraisal rails-6.0 rake
+DB=sqlite bundle exec appraisal rails-6.1 rake
+DB=sqlite bundle exec appraisal rails-7.0 rake
+DB=mysql bundle exec appraisal rails-7.0 rake
createuser --superuser postgres
-DB=postgres bundle exec appraisal rails-5.2 rake
+DB=postgres bundle exec appraisal rails-7.0 rake
```
## The dummy_app
@@ -80,7 +69,7 @@ In the rare event you need a `console` in the `dummy_app`:
```
cd spec/dummy_app
cp config/database.mysql.yml config/database.yml
-BUNDLE_GEMFILE='../../gemfiles/rails_5.2.gemfile' bin/rails console -e test
+BUNDLE_GEMFILE='../../gemfiles/rails_7.0.gemfile' bin/rails console -e test
```
## Adding new schema
@@ -139,4 +128,4 @@ markdown-toc -i --maxdepth 3 --bullets='-' README.md
1. cherry-pick the "Release 10.3.0" commit from the `10-stable` branch
1. git push origin master
-[1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md
+[1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index dff3b54..4122303 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -32,11 +32,11 @@ require "bundler/inline"
# STEP ONE: What versions are you using?
gemfile(true) do
- ruby "2.5.1"
+ ruby "3.0.2"
source "https://rubygems.org"
- gem "activerecord", "5.2.0"
+ gem "activerecord", "6.1.4.1"
gem "minitest", "5.11.3"
- gem "paper_trail", "9.2.0", require: false
+ gem "paper_trail", "12.1.0", require: false
gem "sqlite3", "1.3.13"
end
diff --git a/.github/ISSUE_TEMPLATE/feature-suggestion.md b/.github/ISSUE_TEMPLATE/feature-suggestion.md
index a3b93d8..be94590 100644
--- a/.github/ISSUE_TEMPLATE/feature-suggestion.md
+++ b/.github/ISSUE_TEMPLATE/feature-suggestion.md
@@ -15,7 +15,7 @@ closed without comment.
**Is your feature suggestion related to a problem? Please describe.**
A clear and concise description of the problem. You may find the
-[bug report template](https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md)
+[bug report template](https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md)
helpful.
**Describe the solution you'd like to build**
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 15841af..9d64563 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -3,12 +3,19 @@ on:
schedule:
- cron: '30 1 * * *'
+permissions:
+ contents: read
+
jobs:
stale:
+ permissions:
+ issues: write # for actions/stale to close stale issues
+ pull-requests: write # for actions/stale to close stale PRs
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v3
+ - uses: actions/stale@v6
with:
+ exempt-issue-labels: keep
stale-issue-message: >
This issue has been automatically marked as stale due to inactivity.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8ecdb41..cb1bd54 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,5 +1,8 @@
name: gha-workflow-pt-test
on: [push, pull_request]
+permissions:
+ contents: read
+
jobs:
# Linting is a separate job, primary because it only needs to be done once,
@@ -14,7 +17,7 @@ jobs:
uses: ruby/setup-ruby@v1
with:
# See "Lowest supported ruby version" in CONTRIBUTING.md
- ruby-version: '2.5'
+ ruby-version: '2.7'
- name: Bundle
run: |
gem install bundler
@@ -56,20 +59,14 @@ jobs:
# have set this up with each database as a separate job, but then we'd be
# duplicating the matrix configuration three times.
matrix:
- gemfile: [ 'rails_5.2', 'rails_6.0', 'rails_6.1' ]
+ gemfile: [ 'rails_6.0', 'rails_6.1', 'rails_7.0' ]
# To keep matrix size down, only test highest and lowest rubies.
# Ruby 3.0 is an exception. For now, let's continue to test against 2.7
# in case it still produces any deprecation warnings.
#
# See "Lowest supported ruby version" in CONTRIBUTING.md
- ruby: [ '2.5', '2.7', '3.0' ]
-
- exclude:
- # rails 5.2 requires ruby < 3.0
- # https://github.com/rails/rails/issues/40938
- - ruby: '3.0'
- gemfile: 'rails_5.2'
+ ruby: [ '2.7', '3.0', '3.1' ]
steps:
- name: Checkout source
uses: actions/checkout@v2
@@ -77,10 +74,7 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- - name: Bundle
- run: |
- gem install bundler
- bundle install --jobs 4 --retry 3
+ bundler-cache: true # 'bundle install' and cache
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
diff --git a/.rubocop.yml b/.rubocop.yml
index 1f7d143..a99e963 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -2,6 +2,7 @@ require:
- rubocop-packaging
- rubocop-performance
- rubocop-rails
+ - rubocop-rake
- rubocop-rspec
inherit_from: .rubocop_todo.yml
@@ -13,25 +14,24 @@ inherit_from: .rubocop_todo.yml
# - Only include permanent config; temporary goes in .rubocop_todo.yml
AllCops:
+ # Generated files, like schema.rb, are out of our control.
Exclude:
- - gemfiles/vendor/bundle/**/* # This dir only shows up on travis ¯\_(ツ)_/¯
- - spec/dummy_app/db/schema.rb # Generated, out of our control
+ - gemfiles/*
+ - spec/dummy_app/db/schema.rb
# Enable pending cops so we can adopt the code before they are switched on.
NewCops: enable
# See "Lowest supported ruby version" in CONTRIBUTING.md
- TargetRubyVersion: 2.5
-
-Bundler/OrderedGems:
- Exclude:
- - gemfiles/* # generated by Appraisal
+ TargetRubyVersion: 2.7
Layout/ArgumentAlignment:
EnforcedStyle: with_fixed_indentation
+# This cop has a bug in 1.22.2 (https://github.com/rubocop/rubocop/issues/10210)
+# When the bug is fixed, we'll return to using `EnforcedStyle: trailing`.
Layout/DotPosition:
- EnforcedStyle: trailing
+ Enabled: false
# Avoid blank lines inside methods. They are a sign that the method is too big.
Layout/EmptyLineAfterGuardClause:
@@ -56,20 +56,11 @@ Layout/MultilineOperationIndentation:
Layout/ParameterAlignment:
EnforcedStyle: with_fixed_indentation
-Layout/SpaceAroundMethodCallOperator:
- Enabled: true
-
# Use exactly one space on each side of an operator. Do not align operators
# because it makes the code harder to edit, and makes lines unnecessarily long.
Layout/SpaceAroundOperators:
AllowForAlignment: false
-Lint/RaiseException:
- Enabled: true
-
-Lint/StructNewOverride:
- Enabled: true
-
# Migrations often contain long up/down methods, and extracting smaller methods
# from these is of questionable value.
Metrics/AbcSize:
@@ -99,12 +90,9 @@ Naming/FileName:
- Appraisals
# Heredocs are usually assigned to a variable or constant, which already has a
-# name, so naming the heredoc doesn't add much value. Feel free to name
-# heredocs that are used as anonymous values (not a variable, constant, or
-# named parameter).
-#
-# All heredocs containing SQL should be named SQL, to support editor syntax
-# highlighting.
+# name, so naming the delimiter doesn't add much value unless doing so improves
+# syntax highlighting. For example, all heredocs containing SQL should be named
+# SQL, to support editor syntax highlighting.
Naming/HeredocDelimiterNaming:
Enabled: false
@@ -117,13 +105,26 @@ Naming/PredicateName:
Naming/MethodParameterName:
Enabled: false
-# This cop does not seem to work in rubocop-rspec 1.28.0
-RSpec/DescribeClass:
+# This cop has low value to begin with. Also, secondarily, it does not allow
+# reasonable names like `rails_lt_6_0`.
+Naming/VariableNumber:
Enabled: false
-# This cop has a bug in 1.35.0
-# https://github.com/rubocop-hq/rubocop-rspec/issues/799
-RSpec/DescribedClass:
+# A valuable optimization in production code, but not valuable in specs.
+Performance/CollectionLiteralInLoop:
+ Exclude:
+ - spec/**/*
+
+# This cop only applies to app dev, not gem dev.
+Rails/RakeEnvironment:
+ Enabled: false
+
+# Good advice for rails applications, but not applicable to libraries like PT.
+Rails/SkipsModelValidations:
+ Enabled: false
+
+# This cop does not seem to work in rubocop-rspec 1.28.0
+RSpec/DescribeClass:
Enabled: false
# Yes, ideally examples would be short. Is it possible to pick a limit and say,
@@ -149,10 +150,6 @@ Style/BlockDelimiters:
Style/DoubleNegation:
Enabled: false
-# This cop is unimportant in this repo.
-Style/ExponentialNotation:
- Enabled: false
-
# Avoid annotated tokens except in desperately complicated format strings.
# In 99% of format strings they actually make it less readable.
Style/FormatStringToken:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 03f21de..4c60969 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,129 +1,23 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
-# on 2021-03-21 03:46:53 UTC using RuboCop version 1.11.0.
+# on 2022-11-26 07:45:38 UTC using RuboCop version 1.22.3.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
-# Include: **/*.gemspec
-Gemspec/OrderedDependencies:
- Exclude:
- - 'paper_trail.gemspec'
-
-# Offense count: 5
-# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
-Metrics/AbcSize:
- Max: 19 # Goal: the default (17?)
-
-# Offense count: 1
-# Configuration parameters: IgnoredMethods.
-Metrics/CyclomaticComplexity:
- Max: 8 # Goal: 7
-
-# Offense count: 1
-# Configuration parameters: IgnoredMethods.
-Metrics/PerceivedComplexity:
- Max: 9 # Goal: 7
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Performance/BlockGivenWithExplicitBlock:
- Exclude:
- - 'lib/paper_trail.rb'
-
-# Offense count: 1
-# Configuration parameters: MinSize.
-Performance/CollectionLiteralInLoop:
- Exclude:
- - 'spec/models/version_spec.rb'
-
-# Offense count: 115
-# Configuration parameters: Prefixes.
-# Prefixes: when, with, without
-RSpec/ContextWording:
- Enabled: false
-
# It may be possible for us to use safe_load, but we'd have to pass the
# safelists, like `whitelist_classes` into our serializer, and the serializer
# interface is a public API, so that would be a breaking change.
+# Offense count: 13
+# Cop supports --auto-correct.
Security/YAMLLoad:
Exclude:
- 'lib/paper_trail/serializers/yaml.rb'
+ - 'spec/models/book_spec.rb'
- 'spec/models/gadget_spec.rb'
- 'spec/models/no_object_spec.rb'
- 'spec/models/person_spec.rb'
- 'spec/models/version_spec.rb'
- 'spec/paper_trail/events/destroy_spec.rb'
- - 'spec/paper_trail/model_spec.rb'
- - 'spec/paper_trail/serializer_spec.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/ApplicationController:
- Exclude:
- - 'spec/dummy_app/app/controllers/test_controller.rb'
-
-# Offense count: 56
-# Cop supports --auto-correct.
-Rails/ApplicationRecord:
- Enabled: false
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/NegateInclude:
- Exclude:
- - 'lib/paper_trail/events/base.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/Presence:
- Exclude:
- - 'lib/paper_trail/reifier.rb'
-
-# Offense count: 2
-# Cop supports --auto-correct.
-# Configuration parameters: Include.
-# Include: **/Rakefile, **/*.rake
-Rails/RakeEnvironment:
- Exclude:
- - 'lib/capistrano/tasks/**/*.rake'
- - 'Rakefile'
-
-# Offense count: 8
-# Cop supports --auto-correct.
-Rails/RedundantForeignKey:
- Exclude:
- - 'spec/dummy_app/app/models/family/family.rb'
- - 'spec/dummy_app/app/models/family/family_line.rb'
- - 'spec/dummy_app/app/models/person.rb'
-
-# Offense count: 28
-# Configuration parameters: ForbiddenMethods, AllowedMethods.
-# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
-Rails/SkipsModelValidations:
- Exclude:
- - 'lib/paper_trail/record_trail.rb'
- - 'spec/models/gadget_spec.rb'
- - 'spec/models/on/create_spec.rb'
- - 'spec/models/on/empty_array_spec.rb'
- - 'spec/models/on/touch_spec.rb'
- - 'spec/models/on/update_spec.rb'
- - 'spec/models/widget_spec.rb'
- - 'spec/paper_trail/cleaner_spec.rb'
- - 'spec/paper_trail/config_spec.rb'
- - 'spec/paper_trail/model_spec.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/WhereNot:
- Exclude:
- - 'lib/paper_trail/version_concern.rb'
-
-RSpec/FilePath:
- Exclude:
- - 'spec/paper_trail/model_spec.rb'
- 'spec/paper_trail/serializer_spec.rb'
diff --git a/Appraisals b/Appraisals
index b4051e7..13a6df1 100644
--- a/Appraisals
+++ b/Appraisals
@@ -8,12 +8,6 @@
# > appraisal. If something is specified in both the Gemfile and an appraisal,
# > the version from the appraisal takes precedence.
# > https://github.com/thoughtbot/appraisal
-#
-#
-appraise "rails-5.2" do
- gem "rails", "~> 5.2.4"
- gem "rails-controller-testing", "~> 1.0.2"
-end
appraise "rails-6.0" do
gem "rails", "~> 6.0.3"
@@ -24,3 +18,8 @@ appraise "rails-6.1" do
gem "rails", "~> 6.1.0"
gem "rails-controller-testing", "~> 1.0.5"
end
+
+appraise "rails-7.0" do
+ gem "rails", "~> 7.0.3.1"
+ gem "rails-controller-testing", "~> 1.0.5"
+end
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fad120..f29d57c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,10 +17,142 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
- None
+## 14.0.0 (2022-11-26)
+
+### Breaking Changes
+
+- [#1399](https://github.com/paper-trail-gem/paper_trail/pull/1399) - Same
+ change re: `YAML.safe_load` as in 13.0.0, but this time for Rails 6.0 and 6.1.
+- [#1406](https://github.com/paper-trail-gem/paper_trail/pull/1406) -
+ Certain [Metadata][1] keys are now forbidden, like `id`, and `item_type`.
+ These keys are reserved by PT.
+
+### Dependencies
+
+- Drop support for Rails 5.2, which reached EoL on 2022-06-01
+- Drop support for Ruby 2.6, which reached EoL on 2022-03-31
+- Drop support for request_store < 1.4
+
+### Added
+
+- None
+
+### Fixed
+
+- [#1395](https://github.com/paper-trail-gem/paper_trail/issues/1395) -
+ Fix incorrect `Version#created_at` value when using
+ `PaperTrail::RecordTrail#update_columns`
+- [#1404](https://github.com/paper-trail-gem/paper_trail/pull/1404) -
+ Delay referencing ActiveRecord until after Railtie is loaded
+- Where possible, methods which are not part of PaperTrail's public API have
+ had their access changed to private. All of these methods had been clearly
+ marked as `@api private` in the documentation, for years. This is not expected
+ to be a breaking change.
+
+## 13.0.0 (2022-08-15)
+
+### Breaking Changes
+
+- For Rails >= 7.0, the default serializer will now use `YAML.safe_load` unless
+ `ActiveRecord.use_yaml_unsafe_load`. This change only affects users whose
+ `versions` table has `object` or `object_changes` columns of type `text`, and
+ who use the YAML serializer. People who use the JSON serializer, or those with
+ `json(b)` columns, are unaffected. Please see
+ [doc/pt_13_yaml_safe_load.md](doc/pt_13_yaml_safe_load.md) for details.
+
+### Added
+
+- None
+
+### Fixed
+
+- None
+
+## 12.3.0 (2022-03-13)
+
+### Breaking Changes
+
+- None
+
+### Added
+
+- [#1371](https://github.com/paper-trail-gem/paper_trail/pull/1371) - Added
+ `in_after_callback` argument to `PaperTrail::RecordTrail#save_with_version`,
+ to allow the caller to indicate if this method is being called during an
+ `after` callback. Defaults to `false`.
+- [#1374](https://github.com/paper-trail-gem/paper_trail/pull/1374) - Added
+ option `--uuid` when generating new migration. This can be used to set the
+ type of item_id column to uuid for use with paper_trail on a database that
+ uses uuid as primary key.
+
+### Fixed
+
+- [#1373](https://github.com/paper-trail-gem/paper_trail/issues/1373) - Add
+ CLI option to use uuid type for item_id when generating migration.
+- [#1376](https://github.com/paper-trail-gem/paper_trail/pull/1376) - Create a
+ version record when associated object is touched. Restores the behavior of
+ PaperTrail < v12.1.0.
+
+## 12.2.0 (2022-01-21)
+
+### Breaking Changes
+
+- None
+
+### Added
+
+- [#1365](https://github.com/paper-trail-gem/paper_trail/pull/1365) -
+ Support Rails 7.0
+- [#1349](https://github.com/paper-trail-gem/paper_trail/pull/1349) -
+ `if:` and `unless:` work with `touch` events now.
+
+### Fixed
+
+- [#1366](https://github.com/paper-trail-gem/paper_trail/pull/1366) -
+ Fixed a bug where the `create_versions` migration lead to a broken `db/schema.rb` for Ruby 3
+
+### Dependencies
+
+- [#1338](https://github.com/paper-trail-gem/paper_trail/pull/1338) -
+ Support Psych version 4
+- ruby >= 2.6 (was >= 2.5). Ruby 2.5 reached EoL on 2021-03-31.
+
+## 12.1.0 (2021-08-30)
+
+### Breaking Changes
+
+- None
+
+### Added
+
+- [#1292](https://github.com/paper-trail-gem/paper_trail/pull/1292) -
+ `where_attribute_changes` queries for versions where the object's attribute
+ changed to or from any values.
+- [#1291](https://github.com/paper-trail-gem/paper_trail/pull/1291) -
+ `where_object_changes_to` queries for versions where the object's attributes
+ changed to one set of known values from any other set of values.
+
+### Fixed
+
+- [#1285](https://github.com/paper-trail-gem/paper_trail/pull/1285) -
+ For ActiveRecord >= 6.0, the `touch` callback will no longer create a new
+ `Version` for skipped or ignored attributes.
+- [#1309](https://github.com/paper-trail-gem/paper_trail/pull/1309) -
+ Removes `item_subtype` requirement when specifying model-specific limits.
+- [#1333](https://github.com/paper-trail-gem/paper_trail/pull/1333) -
+ Improve reification of STI models that use `find_sti_class`/`sti_class_for`
+ to customize single table inheritance.
+
## 12.0.0 (2021-03-29)
### Breaking Changes
+- [#1281](https://github.com/paper-trail-gem/paper_trail/pull/1281) Rails:
+ Instead of an `Engine`, PT now provides a `Railtie`, which is simpler.
+ This was not expected to be a breaking change, but has caused trouble for
+ some people:
+ - Issue with the deprecated `autoloader = :classic` setting
+ (https://github.com/paper-trail-gem/paper_trail/issues/1305)
- Rails: The deprecated `config.paper_trail` configuration technique
has been removed. This configuration object was deprecated in 10.2.0. It only
had one key, `config.paper_trail.enabled`. Please review docs section [2.d.
@@ -1272,3 +1404,5 @@ in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is use
- [#160](https://github.com/paper-trail-gem/paper_trail/pull/160) - Fixed failing tests and resolved out of date dependency issues.
- [#157](https://github.com/paper-trail-gem/paper_trail/pull/157) - Refactored `class_attribute` names on the `ClassMethods` module
for names that are not obviously pertaining to PaperTrail to prevent method name collision.
+
+[1]: https://github.com/paper-trail-gem/paper_trail#4c-storing-metadata
diff --git a/README.md b/README.md
index bc2f411..22f6947 100644
--- a/README.md
+++ b/README.md
@@ -10,21 +10,25 @@ has been destroyed.
## Documentation
-| Version | Documentation |
-| -------------- | ------------- |
-| Unreleased | https://github.com/paper-trail-gem/paper_trail/blob/master/README.md |
-| 12.0.0 | https://github.com/paper-trail-gem/paper_trail/blob/v12.0.0/README.md |
-| 11.1.0 | https://github.com/paper-trail-gem/paper_trail/blob/v11.1.0/README.md |
-| 10.3.1 | https://github.com/paper-trail-gem/paper_trail/blob/v10.3.1/README.md |
-| 9.2.0 | https://github.com/paper-trail-gem/paper_trail/blob/v9.2.0/README.md |
-| 8.1.2 | https://github.com/paper-trail-gem/paper_trail/blob/v8.1.2/README.md |
-| 7.1.3 | https://github.com/paper-trail-gem/paper_trail/blob/v7.1.3/README.md |
-| 6.0.2 | https://github.com/paper-trail-gem/paper_trail/blob/v6.0.2/README.md |
-| 5.2.3 | https://github.com/paper-trail-gem/paper_trail/blob/v5.2.3/README.md |
-| 4.2.0 | https://github.com/paper-trail-gem/paper_trail/blob/v4.2.0/README.md |
-| 3.0.9 | https://github.com/paper-trail-gem/paper_trail/blob/v3.0.9/README.md |
-| 2.7.2 | https://github.com/paper-trail-gem/paper_trail/blob/v2.7.2/README.md |
-| 1.6.5 | https://github.com/paper-trail-gem/paper_trail/blob/v1.6.5/README.md |
+This is the _user guide_. See also, the
+[API reference](https://www.rubydoc.info/gems/paper_trail).
+
+Choose version:
+[Unreleased](https://github.com/paper-trail-gem/paper_trail/blob/master/README.md),
+[14.0](https://github.com/paper-trail-gem/paper_trail/blob/v14.0.0/README.md),
+[13.0](https://github.com/paper-trail-gem/paper_trail/blob/v13.0.0/README.md),
+[12.3](https://github.com/paper-trail-gem/paper_trail/blob/v12.3.0/README.md),
+[11.1](https://github.com/paper-trail-gem/paper_trail/blob/v11.1.0/README.md),
+[10.3](https://github.com/paper-trail-gem/paper_trail/blob/v10.3.1/README.md),
+[9.2](https://github.com/paper-trail-gem/paper_trail/blob/v9.2.0/README.md),
+[8.1](https://github.com/paper-trail-gem/paper_trail/blob/v8.1.2/README.md),
+[7.1](https://github.com/paper-trail-gem/paper_trail/blob/v7.1.3/README.md),
+[6.0](https://github.com/paper-trail-gem/paper_trail/blob/v6.0.2/README.md),
+[5.2](https://github.com/paper-trail-gem/paper_trail/blob/v5.2.3/README.md),
+[4.2](https://github.com/paper-trail-gem/paper_trail/blob/v4.2.0/README.md),
+[3.0](https://github.com/paper-trail-gem/paper_trail/blob/v3.0.9/README.md),
+[2.7](https://github.com/paper-trail-gem/paper_trail/blob/v2.7.2/README.md),
+[1.6](https://github.com/paper-trail-gem/paper_trail/blob/v1.6.5/README.md)
## Table of Contents
@@ -47,6 +51,8 @@ has been destroyed.
- [3.b. Navigating Versions](#3b-navigating-versions)
- [3.c. Diffing Versions](#3c-diffing-versions)
- [3.d. Deleting Old Versions](#3d-deleting-old-versions)
+ - [3.e. Queries](#3e-queries)
+ - [3.f. Defunct `item_id`s](#3f-defunct-item_ids)
- [4. Saving More Information About Versions](#4-saving-more-information-about-versions)
- [4.a. Finding Out Who Was Responsible For A Change](#4a-finding-out-who-was-responsible-for-a-change)
- [4.b. Associations](#4b-associations)
@@ -60,6 +66,7 @@ has been destroyed.
- [6.a. Custom Version Classes](#6a-custom-version-classes)
- [6.b. Custom Serializer](#6b-custom-serializer)
- [6.c. Custom Object Changes](#6c-custom-object-changes)
+ - [6.d. Excluding the Object Column](#6d-excluding-the-object-column)
- [7. Testing](#7-testing)
- [7.a. Minitest](#7a-minitest)
- [7.b. RSpec](#7b-rspec)
@@ -82,28 +89,30 @@ has been destroyed.
### 1.a. Compatibility
-| paper_trail | branch | ruby | activerecord |
-| -------------- | ---------- | -------- | ------------- |
-| unreleased | master | >= 2.5.0 | >= 5.2, < 6.2 |
-| 12 | master | >= 2.5.0 | >= 5.2, < 6.2 |
-| 11 | master | >= 2.4.0 | >= 5.2, < 6.1 |
-| 10 | 10-stable | >= 2.3.0 | >= 4.2, < 6.1 |
-| 9 | 9-stable | >= 2.3.0 | >= 4.2, < 5.3 |
-| 8 | 8-stable | >= 2.2.0 | >= 4.2, < 5.2 |
-| 7 | 7-stable | >= 2.1.0 | >= 4.0, < 5.2 |
-| 6 | 6-stable | >= 1.9.3 | >= 4.0, < 5.2 |
-| 5 | 5-stable | >= 1.9.3 | >= 3.0, < 5.1 |
-| 4 | 4-stable | >= 1.8.7 | >= 3.0, < 5.1 |
-| 3 | 3.0-stable | >= 1.8.7 | >= 3.0, < 5 |
-| 2 | 2.7-stable | >= 1.8.7 | >= 3.0, < 4 |
-| 1 | rails2 | >= 1.8.7 | >= 2.3, < 3 |
+| paper_trail | branch | ruby | activerecord |
+|-------------|------------|----------|---------------|
+| unreleased | master | >= 2.7.0 | >= 6.0, < 7.1 |
+| 14 | 14-stable | >= 2.7.0 | >= 6.0, < 7.1 |
+| 13 | 13-stable | >= 2.6.0 | >= 5.2, < 7.1 |
+| 12 | 12-stable | >= 2.6.0 | >= 5.2, < 7.1 |
+| 11 | 11-stable | >= 2.4.0 | >= 5.2, < 6.1 |
+| 10 | 10-stable | >= 2.3.0 | >= 4.2, < 6.1 |
+| 9 | 9-stable | >= 2.3.0 | >= 4.2, < 5.3 |
+| 8 | 8-stable | >= 2.2.0 | >= 4.2, < 5.2 |
+| 7 | 7-stable | >= 2.1.0 | >= 4.0, < 5.2 |
+| 6 | 6-stable | >= 1.9.3 | >= 4.0, < 5.2 |
+| 5 | 5-stable | >= 1.9.3 | >= 3.0, < 5.1 |
+| 4 | 4-stable | >= 1.8.7 | >= 3.0, < 5.1 |
+| 3 | 3.0-stable | >= 1.8.7 | >= 3.0, < 5 |
+| 2 | 2.7-stable | >= 1.8.7 | >= 3.0, < 4 |
+| 1 | rails2 | >= 1.8.7 | >= 2.3, < 3 |
Experts: to install incompatible versions of activerecord, see
`paper_trail/compatibility.rb`.
### 1.b. Installation
-1. Add PaperTrail to your `Gemfile`.
+1. Add PaperTrail to your `Gemfile` and run [`bundle`][57].
`gem 'paper_trail'`
@@ -112,16 +121,13 @@ Experts: to install incompatible versions of activerecord, see
```
bundle exec rails generate paper_trail:install [--with-changes]
```
+
+ If tables in your project use `uuid` instead of `integers` for `id`, then use:
+ ```
+ bundle exec rails generate paper_trail:install [--uuid]
+ ```
- For more information on this generator, see [section 5.c.
- Generators](#5c-generators).
-
- If using [rails_admin][38], you must enable the
- experimental [Associations](#4b-associations) feature.
-
- If you're getting "Could not find generator 'paper_trail:install'" errors from
- recent Ruby/Rails versions, try running `spring stop`
- (see [this thread](https://github.com/paper-trail-gem/paper_trail/issues/459) for more details).
+ See [section 5.c. Generators](#5c-generators) for details.
```
bundle exec rake db:migrate
@@ -160,7 +166,7 @@ Once you have a version, you can find out what happened:
```ruby
v = widget.versions.last
-v.event # 'update', 'create', 'destroy'. See also: Custom Event Names
+v.event # 'update', 'create', 'destroy'. See also: "The versions.event Column"
v.created_at
v.whodunnit # ID of `current_user`. Requires `set_paper_trail_whodunnit` callback.
widget = v.reify # The widget as it was before the update (nil for a create event)
@@ -374,8 +380,8 @@ end
```
The `paper_trail.on_destroy` method can be further configured to happen
-`:before` or `:after` the destroy event. In PaperTrail 4, the default is
-`:after`. In PaperTrail 5, the default will be `:before`, to support
+`:before` or `:after` the destroy event. Until PaperTrail 4, the default was
+`:after`. Starting with PaperTrail 5, the default is `:before`, to support
ActiveRecord 5. (see https://github.com/paper-trail-gem/paper_trail/pull/683)
### 2.b. Choosing When To Save New Versions
@@ -411,7 +417,7 @@ my_model.paper_trail.save_with_version
#### Ignore
-You can `ignore` changes to certain attributes:
+If you don't want a version created when only a certain attribute changes, you can `ignore` that attribute:
```ruby
class Article < ActiveRecord::Base
@@ -432,6 +438,8 @@ a.versions.length # 2
a.paper_trail.previous_version.title # 'My Title'
```
+Note: ignored fields will be stored in the version records. If you want to keep a field out of the versions table, use [`:skip`](#skip) instead of `:ignore`; skipped fields are also implicitly ignored.
+
The `:ignore` option can also accept `Hash` arguments that we are considering deprecating.
```ruby
@@ -495,17 +503,31 @@ article being saved if a changed attribute is included in `:only` but not in
#### Skip
-You can skip attributes completely with the `:skip` option. As with `:ignore`,
+If you never want a field's values in the versions table, you can `:skip` the attribute. As with `:ignore`,
updates to these attributes will not create a version record. In addition, if a
version record is created for some other reason, these attributes will not be
persisted.
```ruby
-class Article < ActiveRecord::Base
- has_paper_trail skip: [:file_upload]
+class Author < ActiveRecord::Base
+ has_paper_trail skip: [:social_security_number]
end
```
+Author's social security numbers will never appear in the versions log, and if an author updates only their social security number, it won't create a version record.
+
+#### Comparing `:ignore`, `:only`, and `:skip`
+
+- `:only` is basically the same as `:ignore`, but its inverse.
+- `:ignore` controls whether paper_trail will create a version record or not.
+- `:skip` controls whether paper_trail will save that field with the version record.
+- Skipped fields are also implicitly ignored. paper_trail does this internally.
+- Ignored fields are not implicitly skipped.
+
+So:
+- Ignore a field if you don't want a version record created when it's the only field to change.
+- Skip a field if you don't want it to be saved with any version records.
+
### 2.d. Turning PaperTrail Off
PaperTrail is on by default, but sometimes you don't want to record versions.
@@ -616,10 +638,6 @@ has_paper_trail limit: 2
has_paper_trail limit: nil
```
-To use a per-model limit, your `versions` table must have an
-`item_subtype` column. See [Section
-4.b.1](https://github.com/paper-trail-gem/paper_trail#4b1-the-optional-item_subtype-column).
-
## 3. Working With Versions
### 3.a. Reverting And Undeleting A Model
@@ -710,34 +728,20 @@ widget = widget.paper_trail.previous_version
widget.paper_trail.live? # false
```
-And you can perform `WHERE` queries for object versions based on attributes:
-
-```ruby
-# Find versions that meet these criteria.
-PaperTrail::Version.where_object(content: 'Hello', title: 'Article')
-
-# Find versions before and after attribute `atr` had value `v`:
-PaperTrail::Version.where_object_changes(atr: 'v')
-```
-
-Using `where_object_changes` to read YAML from a text column was deprecated in
-8.1.0, and will now raise an error.
+See also: Section 3.e. Queries
### 3.c. Diffing Versions
There are two scenarios: diffing adjacent versions and diffing non-adjacent
versions.
-The best way to diff adjacent versions is to get PaperTrail to do it for you.
-If you add an `object_changes` text column to your `versions` table, either at
-installation time with the `rails generate paper_trail:install --with-changes`
-option or manually, PaperTrail will store the `changes` diff (excluding any
-attributes PaperTrail is ignoring) in each `update` version. You can use the
-`version.changeset` method to retrieve it. For example:
+The best way to diff adjacent versions is to get PaperTrail to do it for you. If
+you add an `object_changes` column to your `versions` table, PaperTrail will
+store the `changes` diff in each version. Ignored attributes are omitted.
```ruby
widget = Widget.create name: 'Bob'
-widget.versions.last.changeset
+widget.versions.last.changeset # reads object_changes column
# {
# "name"=>[nil, "Bob"],
# "created_at"=>[nil, 2015-08-10 04:10:40 UTC],
@@ -758,11 +762,12 @@ widget.versions.last.changeset
Prior to 10.0.0, the `object_changes` were only stored for create and update
events. As of 10.0.0, they are stored for all three events.
-Please be aware that PaperTrail doesn't use diffs internally. When I designed
-PaperTrail I wanted simplicity and robustness so I decided to make each version
-of an object self-contained. A version stores all of its object's data, not a
-diff from the previous version. This means you can delete any version without
-affecting any other.
+PaperTrail doesn't use diffs internally.
+
+> When I designed PaperTrail I wanted simplicity and robustness so I decided to
+> make each version of an object self-contained. A version stores all of its
+> object's data, not a diff from the previous version. This means you can
+> delete any version without affecting any other. -Andy
To diff non-adjacent versions you'll have to write your own code. These
libraries may help:
@@ -777,12 +782,7 @@ For diffing two strings:
or arbitrary-boundary-string-wise diffs. Works very well on non-HTML input.
* [diff-lcs][21]: old-school, line-wise diffs.
-For diffing two ActiveRecord objects:
-
-* [Jeremy Weiskotten's PaperTrail fork][22]: uses ActiveSupport's diff to return
- an array of hashes of the changes.
-* [activerecord-diff][23]: rather like ActiveRecord::Dirty but also allows you
- to specify which columns to compare.
+Unfortunately, there is no currently widely available and supported library for diffing two ActiveRecord objects.
### 3.d. Deleting Old Versions
@@ -798,6 +798,57 @@ sql> delete from versions where created_at < '2010-06-01';
PaperTrail::Version.where('created_at < ?', 1.day.ago).delete_all
```
+### 3.e. Queries
+
+You can query records in the `versions` table based on their `object` or
+`object_changes` columns.
+
+```ruby
+# Find versions that meet these criteria.
+PaperTrail::Version.where_object(content: 'Hello', title: 'Article')
+
+# Find versions before and after attribute `atr` had value `v`:
+PaperTrail::Version.where_object_changes(atr: 'v')
+```
+
+See also:
+
+- `where_object_changes_from`
+- `where_object_changes_to`
+- `where_attribute_changes`
+
+Only `where_object` supports text columns. Your `object_changes` column should
+be a `json` or `jsonb` column if possible. If you must use a `text` column,
+you'll have to write a [custom
+`object_changes_adapter`](#6c-custom-object-changes).
+
+### 3.f. Defunct `item_id`s
+
+The `item_id`s in your `versions` table can become defunct over time,
+potentially causing application errors when `id`s in the foreign table are
+reused. `id` reuse can be an explicit choice of the application, or implicitly
+caused by sequence cycling. The chance of `id` reuse is reduced (but not
+eliminated) with `bigint` `id`s or `uuid`s, `no cycle`
+[sequences](https://www.postgresql.org/docs/current/sql-createsequence.html),
+and/or when `versions` are periodically deleted.
+
+Ideally, a Foreign Key Constraint (FKC) would set `item_id` to null when an item
+is deleted. However, `items` is a polymorphic relationship. A partial FKC (e.g.
+an FKC with a `where` clause) is possible, but only in Postgres, and it is
+impractical to maintain FKCs for every versioned table unless the number of
+such tables is very small.
+
+If [per-table `Version`
+classes](https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes)
+are used, then a partial FKC is no longer needed. So, a normal FKC can be
+written in any RDBMS, but it remains impractical to maintain so many FKCs.
+
+Some applications choose to handle this problem by "soft-deleting" versioned
+records, i.e. marking them as deleted instead of actually deleting them. This
+completely prevents `id` reuse, but adds complexity to the application. In most
+applications, this is the only known practical solution to the `id` reuse
+problem.
+
## 4. Saving More Information About Versions
### 4.a. Finding Out Who Was Responsible For A Change
@@ -991,13 +1042,19 @@ PaperTrail::Version.where(author_id: author_id)
inserted into its own columns.
| *PT Column* | *How bad of an idea?* | *Alternative* |
-| -------------- | --------------------- | ----------------------------- |
-| item_type | terrible idea | |
-| item_id | terrible idea | |
+|----------------|-----------------------|-------------------------------|
+| created_at | forbidden* | |
| event | meh | paper_trail_event |
-| whodunnit | meh | PaperTrail.request.whodunnit= |
+| id | forbidden | |
+| item_id | forbidden | |
+| item_subtype | forbidden | |
+| item_type | forbidden | |
| object | a little dangerous | |
| object_changes | a little dangerous | |
+| updated_at | forbidden | |
+| whodunnit | meh | PaperTrail.request.whodunnit= |
+
+\* forbidden - raises a `PaperTrail::InvalidOption` error as of PT 14
## 5. ActiveRecord
@@ -1095,10 +1152,12 @@ Be advised that redefining an association is an undocumented feature of Rails.
### 5.c. Generators
PaperTrail has one generator, `paper_trail:install`. It writes, but does not
-run, a migration file.
-The migration adds (at least) the `versions` table. The
-most up-to-date documentation for this generator can be found by running `rails
-generate paper_trail:install --help`, but a copy is included here for
+run, a migration file. The migration creates the `versions` table.
+
+#### Reference
+
+The most up-to-date documentation for this generator can be found by running
+`rails generate paper_trail:install --help`, but a copy is included here for
convenience.
```
@@ -1107,6 +1166,7 @@ Usage:
Options:
[--with-changes], [--no-with-changes] # Store changeset (diff) with each version
+ [--uuid] # To use paper_trail with projects using uuid for id
Runtime options:
-f, [--force] # Overwrite files that already exist
@@ -1180,17 +1240,20 @@ class PostVersion < PaperTrail::Version
end
```
-If you only use custom version classes and don't have a `versions` table, you
-must let ActiveRecord know that the `PaperTrail::Version` class is an
-`abstract_class`.
+If you only use custom version classes and don't have a `versions` table, you must
+let ActiveRecord know that your base version class (eg. `ApplicationVersion` below)
+class is an `abstract_class`.
```ruby
-# app/models/paper_trail/version.rb
-module PaperTrail
- class Version < ActiveRecord::Base
- include PaperTrail::VersionConcern
- self.abstract_class = true
- end
+# app/models/application_version.rb
+class ApplicationVersion < ActiveRecord::Base
+ include PaperTrail::VersionConcern
+ self.abstract_class = true
+end
+
+class PostVersion < ApplicationVersion
+ self.table_name = :post_versions
+ self.sequence_name = :post_versions_id_seq
end
```
@@ -1363,15 +1426,24 @@ reading `::PaperTrail::Events::Base#recordable_object_changes`.
An adapter can implement any or all of the following methods:
-1. diff: Returns the changeset in the desired format given the changeset in the original format
+1. diff: Returns the changeset in the desired format given the changeset in the
+ original format
2. load_changeset: Returns the changeset for a given version object
-3. where_object_changes: Returns the records resulting from the given hash of attributes.
-4. where_object_changes_from: Returns the records resulting from the given hash of attributes where the attributes changed *from* the provided value(s).
+3. where_object_changes: Returns the records resulting from the given hash of
+ attributes.
+4. where_object_changes_from: Returns the records resulting from the given hash
+ of attributes where the attributes changed *from* the provided value(s).
+5. where_object_changes_to: Returns the records resulting from the given hash of
+ attributes where the attributes changed *to* the provided value(s).
+6. where_attribute_changes: Returns the records where the attribute changed to
+ or from any value.
+
+Depending on your needs, you may choose to implement only a subset of these
+methods.
-Depending on what your adapter does, you may have to implement all three.
+#### Known Adapters
-For an example of a complete and useful adapter, see
-[paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
+- [paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
### 6.d. Excluding the Object Column
@@ -1607,8 +1679,24 @@ require 'paper_trail/frameworks/rspec'
```
## 8. PaperTrail Plugins
+
+- paper_trail-active_record
- [paper_trail-association_tracking][6] - track and reify associations
+- paper_trail-audit
+- paper_trail-background
- [paper_trail-globalid][49] - enhances whodunnit by adding an `actor`
+- paper_trail-hashdiff
+- paper_trail-rails
+- paper_trail-related_changes
+- paper_trail-sinatra
+- paper_trail_actor
+- paper_trail_changes
+- paper_trail_manager
+- paper_trail_scrapbook
+- paper_trail_ui
+- revertible_paper_trail
+- rspec-paper_trail
+- sequel_paper_trail
## 9. Integration with Other Libraries
@@ -1683,8 +1771,6 @@ Released under the MIT licence.
[19]: http://github.com/myobie/htmldiff
[20]: http://github.com/pvande/differ
[21]: https://github.com/halostatue/diff-lcs
-[22]: http://github.com/jeremyw/paper_trail/blob/master/lib/paper_trail/has_paper_trail.rb#L151-156
-[23]: http://github.com/tim/activerecord-diff
[24]: https://github.com/paper-trail-gem/paper_trail/blob/master/lib/paper_trail/serializers/yaml.rb
[25]: https://github.com/paper-trail-gem/paper_trail/blob/master/lib/paper_trail/serializers/json.rb
[26]: http://www.postgresql.org/docs/9.4/static/datatype-json.html
@@ -1718,3 +1804,4 @@ Released under the MIT licence.
[54]: https://rubygems.org/gems/paper_trail
[55]: https://api.dependabot.com/badges/compatibility_score?dependency-name=paper_trail&package-manager=bundler&version-scheme=semver
[56]: https://dependabot.com/compatibility-score.html?dependency-name=paper_trail&package-manager=bundler&version-scheme=semver
+[57]: https://bundler.io/v2.3/man/bundle-install.1.html
diff --git a/Rakefile b/Rakefile
index 3f814c5..0c7ed0c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -38,11 +38,8 @@ task :clean do
end
end
-desc <<~EOS
- Write a database.yml for the specified RDBMS, and create database. Does not
- migrate. Migration happens later in spec_helper.
-EOS
-task prepare: %i[clean install_database_yml] do
+desc "Create the database."
+task :create_db do
puts format("creating %s database", ENV["DB"])
case ENV["DB"]
when "mysql"
@@ -59,6 +56,12 @@ task prepare: %i[clean install_database_yml] do
end
end
+desc <<~EOS
+ Write a database.yml for the specified RDBMS, and create database. Does not
+ migrate. Migration happens later in spec_helper.
+EOS
+task prepare: %i[clean install_database_yml create_db]
+
require "rspec/core/rake_task"
desc "Run tests on PaperTrail with RSpec"
task(:spec).clear
diff --git a/debian/changelog b/debian/changelog
index dc9caa5..f4cb64b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-paper-trail (14.0.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Fri, 20 Jan 2023 06:31:51 -0000
+
ruby-paper-trail (12.0.0-1) unstable; urgency=medium
* Team upload.
diff --git a/doc/bug_report_template.rb b/doc/bug_report_template.rb
index 2f7d639..a093860 100644
--- a/doc/bug_report_template.rb
+++ b/doc/bug_report_template.rb
@@ -1,4 +1,4 @@
# frozen_string_literal: true
-# Moved to .github/ISSUE_TEMPLATE/bug_report.md
+# Moved to .github/ISSUE_TEMPLATE/bug-report.md
# Please update your bookmarks
diff --git a/doc/pt_13_yaml_safe_load.md b/doc/pt_13_yaml_safe_load.md
new file mode 100644
index 0000000..7d060ee
--- /dev/null
+++ b/doc/pt_13_yaml_safe_load.md
@@ -0,0 +1,53 @@
+# PT 13 uses YAML.safe_load
+
+Starting with 13.0.0, in Rails >= 7.0, PT's default serializer
+(`PaperTrail::Serializers::YAML`) will use `safe_load` unless
+`ActiveRecord.use_yaml_unsafe_load`.
+
+PT 14.0.0 extends this protection to Rails 6.
+
+Earlier versions of PT use `unsafe_load`.
+
+## Motivation
+
+> A few days ago Rails released versions 7.0.3.1, 6.1.6.1, 6.0.5.1, and 5.2.8.1.
+> These are security updates that impact applications that use serialised
+> attributes on Active Record models. These updates, identified by CVE-2022-32224
+> cover a possible escalation to RCE when using YAML serialised columns in Active
+> Record.
+> https://rubyonrails.org/2022/7/15/this-week-in-rails-rails-security-releases-improved-generator-option-handling-and-more-24774592
+
+## Who is affected by this change?
+
+This change only affects users whose `versions` table has `object` or
+`object_changes` columns of type `text`, and who use the YAML serializer. People
+who use the JSON serializer, or those with `json(b)` columns, are unaffected.
+
+## To continue using the YAML serializer
+
+We recommend switching to `json(b)` columns, or at least JSON in a `text` column
+(see "Other serializers" below). If you must continue using the YAML serializer,
+PT users are required to configure `ActiveRecord.yaml_column_permitted_classes`
+correctly for their own application. Users may want to start with the following
+safe-list:
+
+```ruby
+::ActiveRecord.use_yaml_unsafe_load = false
+::ActiveRecord.yaml_column_permitted_classes = [
+ ::ActiveRecord::Type::Time::Value,
+ ::ActiveSupport::TimeWithZone,
+ ::ActiveSupport::TimeZone,
+ ::BigDecimal,
+ ::Date,
+ ::Symbol,
+ ::Time
+]
+```
+
+## Other serializers
+
+While YAML remains the default serializer in PT for historical compatibility,
+we have recommended JSON instead, for years. See:
+
+- [PostgreSQL JSON column type support](https://github.com/paper-trail-gem/paper_trail/blob/v12.3.0/README.md#postgresql-json-column-type-support)
+- [Convert existing YAML data to JSON](https://github.com/paper-trail-gem/paper_trail/blob/v12.3.0/README.md#convert-existing-yaml-data-to-json)
diff --git a/doc/triage.md b/doc/triage.md
index 453d6e9..0a9d468 100644
--- a/doc/triage.md
+++ b/doc/triage.md
@@ -13,7 +13,7 @@ For instructions on how to file a bug report, please see our [issue template][3]
[1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/CONTRIBUTING.md
[2]: https://stackoverflow.com/tags/paper-trail-gem
-[3]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md
+[3]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md
```
## Responses to Common Problems
@@ -24,7 +24,7 @@ Hi ___. All issues are required to use our [issue template][2]. See also our
and I'll do what I can to help!
[1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/CONTRIBUTING.md
-[2]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md
+[2]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md
```
## Usage question masquerading as a feature proposal
diff --git a/doc/warning_about_not_setting_whodunnit.md b/doc/warning_about_not_setting_whodunnit.md
index 4cdab97..1bf5058 100644
--- a/doc/warning_about_not_setting_whodunnit.md
+++ b/doc/warning_about_not_setting_whodunnit.md
@@ -9,7 +9,7 @@ After upgrading to PaperTrail 5, you see this warning:
## You want to track whodunnit
Add `before_action :set_paper_trail_whodunnit` to your ApplicationController.
-See the PaperTrail readme for an example (https://git.io/vrsbt).
+See the PaperTrail readme for an example (https://github.com/paper-trail-gem/paper_trail#4a-finding-out-who-was-responsible-for-a-change).
## You don't want to track whodunnit
diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_7.0.gemfile
similarity index 57%
rename from gemfiles/rails_5.2.gemfile
rename to gemfiles/rails_7.0.gemfile
index 4fd4d62..58bdd3c 100644
--- a/gemfiles/rails_5.2.gemfile
+++ b/gemfiles/rails_7.0.gemfile
@@ -2,7 +2,7 @@
source "https://rubygems.org"
-gem "rails", "~> 5.2.4"
-gem "rails-controller-testing", "~> 1.0.2"
+gem "rails", "~> 7.0.3.1"
+gem "rails-controller-testing", "~> 1.0.5"
gemspec path: "../"
diff --git a/lib/generators/paper_trail/install/install_generator.rb b/lib/generators/paper_trail/install/install_generator.rb
index 6fc82fe..1378663 100644
--- a/lib/generators/paper_trail/install/install_generator.rb
+++ b/lib/generators/paper_trail/install/install_generator.rb
@@ -20,6 +20,12 @@ module PaperTrail
default: false,
desc: "Store changeset (diff) with each version"
)
+ class_option(
+ :uuid,
+ type: :boolean,
+ default: false,
+ desc: "Use uuid instead of bigint for item_id type (use only if tables use UUIDs)"
+ )
desc "Generates (but does not run) a migration to add a versions table." \
" See section 5.c. Generators in README.md for more information."
@@ -28,7 +34,8 @@ module PaperTrail
add_paper_trail_migration(
"create_versions",
item_type_options: item_type_options,
- versions_table_options: versions_table_options
+ versions_table_options: versions_table_options,
+ item_id_type_options: item_id_type_options
)
if options.with_changes?
add_paper_trail_migration("add_object_changes_to_versions")
@@ -37,13 +44,18 @@ module PaperTrail
private
+ # To use uuid instead of integer for primary key
+ def item_id_type_options
+ options.uuid? ? "string" : "bigint"
+ end
+
# MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
# See https://github.com/paper-trail-gem/paper_trail/issues/651
def item_type_options
if mysql?
- ", { null: false, limit: 191 }"
+ ", null: false, limit: 191"
else
- ", { null: false }"
+ ", null: false"
end
end
diff --git a/lib/generators/paper_trail/install/templates/create_versions.rb.erb b/lib/generators/paper_trail/install/templates/create_versions.rb.erb
index 268d929..bc44f29 100644
--- a/lib/generators/paper_trail/install/templates/create_versions.rb.erb
+++ b/lib/generators/paper_trail/install/templates/create_versions.rb.erb
@@ -11,7 +11,7 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
def change
create_table :versions<%= versions_table_options %> do |t|
t.string :item_type<%= item_type_options %>
- t.bigint :item_id, null: false
+ t.<%= item_id_type_options %> :item_id, null: false
t.string :event, null: false
t.string :whodunnit
t.text :object, limit: TEXT_BYTES
@@ -29,8 +29,10 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
# version of ActiveRecord with support for fractional seconds in MySQL.
# (https://github.com/rails/rails/pull/14359)
#
+ # MySQL users should use the following line for `created_at`
+ # t.datetime :created_at, limit: 6
t.datetime :created_at
end
- add_index :versions, %i(item_type item_id)
+ add_index :versions, %i[item_type item_id]
end
end
diff --git a/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb b/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb
index f0b8464..9278dac 100644
--- a/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb
+++ b/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb
@@ -7,8 +7,10 @@ module PaperTrail
class UpdateItemSubtypeGenerator < MigrationGenerator
source_root File.expand_path("templates", __dir__)
- desc "Generates (but does not run) a migration to update item_subtype for STI entries in an "\
- "existing versions table."
+ desc(
+ "Generates (but does not run) a migration to update item_subtype for "\
+ "STI entries in an existing versions table."
+ )
def create_migration_file
add_paper_trail_migration("update_versions_for_item_subtype", sti_type_options: options)
diff --git a/lib/paper_trail.rb b/lib/paper_trail.rb
index 51b895b..ba44450 100644
--- a/lib/paper_trail.rb
+++ b/lib/paper_trail.rb
@@ -8,6 +8,13 @@
# can revisit this decision.
require "active_support/all"
+# We used to `require "active_record"` here, but that was [replaced with a
+# Railtie](https://github.com/paper-trail-gem/paper_trail/pull/1281) in PT 12.
+# As a result, we cannot reference `ActiveRecord` in this file (ie. until our
+# Railtie has loaded). If we did, it would cause [problems with non-Rails
+# projects](https://github.com/paper-trail-gem/paper_trail/pull/1401).
+
+require "paper_trail/errors"
require "paper_trail/cleaner"
require "paper_trail/compatibility"
require "paper_trail/config"
@@ -68,7 +75,7 @@ module PaperTrail
#
# @api public
def request(options = nil, &block)
- if options.nil? && !block_given?
+ if options.nil? && !block
Request
else
Request.with(options, &block)
@@ -78,7 +85,7 @@ module PaperTrail
# Set the field which records when a version was created.
# @api public
def timestamp_field=(_field_name)
- raise(E_TIMESTAMP_FIELD_CONFIG)
+ raise Error, E_TIMESTAMP_FIELD_CONFIG
end
# Set the PaperTrail serializer. This setting affects all threads.
@@ -95,7 +102,7 @@ module PaperTrail
# Returns PaperTrail's global configuration object, a singleton. These
# settings affect all threads.
- # @api private
+ # @api public
def config
@config ||= PaperTrail::Config.instance
yield @config if block_given?
@@ -103,6 +110,7 @@ module PaperTrail
end
alias configure config
+ # @api public
def version
VERSION::STRING
end
diff --git a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb
index 5fcdb05..2882706 100644
--- a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb
+++ b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb
@@ -32,11 +32,21 @@ module PaperTrail
if defined_enums[attr] && val.is_a?(::String)
# Because PT 4 used to save the string version of enums to `object_changes`
val
+ elsif rails_gte_7_0? && val.is_a?(ActiveRecord::Type::Time::Value)
+ # Because Rails 7 time attribute throws a delegation error when you deserialize
+ # it with the factory.
+ # See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
+ # https://github.com/rails/rails/issues/43966
+ val.instance_variable_get(:@time)
else
AttributeSerializerFactory.for(@klass, attr).deserialize(val)
end
end
+ def rails_gte_7_0?
+ ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
+ end
+
def serialize(attr, val)
AttributeSerializerFactory.for(@klass, attr).serialize(val)
end
diff --git a/lib/paper_trail/compatibility.rb b/lib/paper_trail/compatibility.rb
index d27031d..a93e696 100644
--- a/lib/paper_trail/compatibility.rb
+++ b/lib/paper_trail/compatibility.rb
@@ -8,7 +8,7 @@ module PaperTrail
#
# It is not safe to assume that a new version of rails will be compatible with
# PaperTrail. PT is only compatible with the versions of rails that it is
- # tested against. See `.travis.yml`.
+ # tested against. See `.github/workflows/test.yml`.
#
# However, as of
# [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
@@ -17,8 +17,8 @@ module PaperTrail
# newer rails versions. Most PT users should avoid incompatible rails
# versions.
module Compatibility
- ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
- ACTIVERECORD_LT = "< 6.2" # not enforced in gemspec
+ ACTIVERECORD_GTE = ">= 6.0" # enforced in gemspec
+ ACTIVERECORD_LT = "< 7.1" # not enforced in gemspec
E_INCOMPATIBLE_AR = <<-EOS
PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
diff --git a/lib/paper_trail/errors.rb b/lib/paper_trail/errors.rb
new file mode 100644
index 0000000..0d3f60a
--- /dev/null
+++ b/lib/paper_trail/errors.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module PaperTrail
+ # Generic PaperTrail exception.
+ # @api public
+ class Error < StandardError
+ end
+
+ # An unexpected option, perhaps a typo, was passed to a public API method.
+ # @api public
+ class InvalidOption < Error
+ end
+
+ # The application's database schema is not supported.
+ # @api public
+ class UnsupportedSchema < Error
+ end
+
+ # The application's database column type is not supported.
+ # @api public
+ class UnsupportedColumnType < UnsupportedSchema
+ def initialize(method:, expected:, actual:)
+ super(
+ format(
+ "%s expected %s column, got %s",
+ method,
+ expected,
+ actual
+ )
+ )
+ end
+ end
+end
diff --git a/lib/paper_trail/events/base.rb b/lib/paper_trail/events/base.rb
index 9a1a226..f84a8b4 100644
--- a/lib/paper_trail/events/base.rb
+++ b/lib/paper_trail/events/base.rb
@@ -22,6 +22,19 @@ module PaperTrail
#
# @api private
class Base
+ E_FORBIDDEN_METADATA_KEY = <<-EOS.squish
+ Forbidden metadata key: %s. As of PT 14, the following metadata keys are
+ forbidden: %s
+ EOS
+ FORBIDDEN_METADATA_KEYS = %i[
+ created_at
+ id
+ item_id
+ item_subtype
+ item_type
+ updated_at
+ ].freeze
+
# @api private
def initialize(record, in_after_callback)
@record = record
@@ -44,6 +57,13 @@ module PaperTrail
private
+ # @api private
+ def assert_metadatum_key_is_permitted(key)
+ return unless FORBIDDEN_METADATA_KEYS.include?(key.to_sym)
+ raise PaperTrail::InvalidOption,
+ format(E_FORBIDDEN_METADATA_KEY, key, FORBIDDEN_METADATA_KEYS)
+ end
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
# https://github.com/paper-trail-gem/paper_trail/pull/899
#
@@ -109,19 +129,25 @@ module PaperTrail
@changed_in_latest_version ||= changes_in_latest_version.keys
end
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
- # https://github.com/paper-trail-gem/paper_trail/pull/899
+ # Memoized to reduce memory usage
#
# @api private
def changes_in_latest_version
- # Memoized to reduce memory usage
- @changes_in_latest_version ||= begin
- if @in_after_callback
- @record.saved_changes
- else
- @record.changes
- end
+ @changes_in_latest_version ||= load_changes_in_latest_version
+ end
+
+ # @api private
+ def evaluate_only
+ only = @record.paper_trail_options[:only].dup
+ # Remove Hash arguments and then evaluate whether the attributes (the
+ # keys of the hash) should also get pushed into the collection.
+ only.delete_if do |obj|
+ obj.is_a?(Hash) &&
+ obj.each { |attr, condition|
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
+ }
end
+ only
end
# An attributed is "ignored" if it is listed in the `:ignore` option
@@ -134,6 +160,18 @@ module PaperTrail
ignored.any? && (changed_in_latest_version & ignored).any?
end
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
+ #
+ # @api private
+ def load_changes_in_latest_version
+ if @in_after_callback
+ @record.saved_changes
+ else
+ @record.changes
+ end
+ end
+
# PT 10 has a new optional column, `item_subtype`
#
# @api private
@@ -157,7 +195,9 @@ module PaperTrail
#
# @api private
def merge_metadata_from_controller_into(data)
- data.merge(PaperTrail.request.controller_info || {})
+ metadata = PaperTrail.request.controller_info || {}
+ metadata.keys.each { |k| assert_metadatum_key_is_permitted(k) }
+ data.merge(metadata)
end
# Updates `data` from the model's `meta` option.
@@ -165,6 +205,7 @@ module PaperTrail
# @api private
def merge_metadata_from_model_into(data)
@record.paper_trail_options[:meta].each do |k, v|
+ assert_metadatum_key_is_permitted(k)
data[k] = model_metadatum(v, data[:event])
end
end
@@ -178,24 +219,32 @@ module PaperTrail
if value.respond_to?(:call)
value.call(@record)
elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
- # If it is an attribute that is changing in an existing object,
- # be sure to grab the current version.
- if event != "create" &&
- @record.has_attribute?(value) &&
- attribute_changed_in_latest_version?(value)
- attribute_in_previous_version(value, false)
- else
- @record.send(value)
- end
+ metadatum_from_model_method(event, value)
else
value
end
end
+ # The model method can either be an attribute or a non-attribute method.
+ #
+ # If it is an attribute that is changing in an existing object,
+ # be sure to grab the correct version.
+ #
+ # @api private
+ def metadatum_from_model_method(event, method)
+ if event != "create" &&
+ @record.has_attribute?(method) &&
+ attribute_changed_in_latest_version?(method)
+ attribute_in_previous_version(method, false)
+ else
+ @record.send(method)
+ end
+ end
+
# @api private
def notable_changes
changes_in_latest_version.delete_if { |k, _v|
- !notably_changed.include?(k)
+ notably_changed.exclude?(k)
}
end
@@ -203,16 +252,9 @@ module PaperTrail
def notably_changed
# Memoized to reduce memory usage
@notably_changed ||= begin
- only = @record.paper_trail_options[:only].dup
- # Remove Hash arguments and then evaluate whether the attributes (the
- # keys of the hash) should also get pushed into the collection.
- only.delete_if do |obj|
- obj.is_a?(Hash) &&
- obj.each { |attr, condition|
- only << attr if condition.respond_to?(:call) && condition.call(@record)
- }
- end
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
+ only = evaluate_only
+ cani = changed_and_not_ignored
+ only.empty? ? cani : (cani & only)
end
end
diff --git a/lib/paper_trail/events/update.rb b/lib/paper_trail/events/update.rb
index 8516bc3..3f50c5c 100644
--- a/lib/paper_trail/events/update.rb
+++ b/lib/paper_trail/events/update.rb
@@ -29,22 +29,38 @@ module PaperTrail
event: @record.paper_trail_event || "update",
whodunnit: PaperTrail.request.whodunnit
}
- if @record.respond_to?(:updated_at)
- data[:created_at] = @record.updated_at
- end
if record_object?
data[:object] = recordable_object(@is_touch)
end
- if record_object_changes?
- changes = @force_changes.nil? ? notable_changes : @force_changes
- data[:object_changes] = prepare_object_changes(changes)
- end
+ merge_object_changes_into(data)
merge_item_subtype_into(data)
merge_metadata_into(data)
end
+ # If it is a touch event, and changed are empty, it is assumed to be
+ # implicit `touch` mutation, and will a version is created.
+ #
+ # See https://github.com/rails/rails/commit/dcb825902d79d0f6baba956f7c6ec5767611353e
+ #
+ # @api private
+ def changed_notably?
+ if @is_touch && changes_in_latest_version.empty?
+ true
+ else
+ super
+ end
+ end
+
private
+ # @api private
+ def merge_object_changes_into(data)
+ if record_object_changes?
+ changes = @force_changes.nil? ? notable_changes : @force_changes
+ data[:object_changes] = prepare_object_changes(changes)
+ end
+ end
+
# `touch` cannot record `object_changes` because rails' `touch` does not
# perform dirty-tracking. Specifically, methods from `Dirty`, like
# `saved_changes`, return the same values before and after `touch`.
diff --git a/lib/paper_trail/model_config.rb b/lib/paper_trail/model_config.rb
index 18e6c74..78576a5 100644
--- a/lib/paper_trail/model_config.rb
+++ b/lib/paper_trail/model_config.rb
@@ -18,11 +18,6 @@ module PaperTrail
`abstract_class`. This is fine, but all application models must be
configured to use concrete (not abstract) version models.
STR
- E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze
- To use PaperTrail's per-model limit in your %s model, you must have an
- item_subtype column in your versions table. See documentation sections
- 2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column.
- STR
DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
Passing versions association name as `has_paper_trail versions: %{versions_name}`
is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
@@ -45,22 +40,14 @@ module PaperTrail
@model_class.after_create { |r|
r.paper_trail.record_create if r.paper_trail.save_version?
}
- return if @model_class.paper_trail_options[:on].include?(:create)
- @model_class.paper_trail_options[:on] << :create
+ append_option_uniquely(:on, :create)
end
# Adds a callback that records a version before or after a "destroy" event.
#
# @api public
def on_destroy(recording_order = "before")
- unless %w[after before].include?(recording_order.to_s)
- raise ArgumentError, 'recording order can only be "after" or "before"'
- end
-
- if recording_order.to_s == "after" && cannot_record_after_destroy?
- raise E_CANNOT_RECORD_AFTER_DESTROY
- end
-
+ assert_valid_recording_order_for_on_destroy(recording_order)
@model_class.send(
"#{recording_order}_destroy",
lambda do |r|
@@ -68,9 +55,7 @@ module PaperTrail
r.paper_trail.record_destroy(recording_order)
end
)
-
- return if @model_class.paper_trail_options[:on].include?(:destroy)
- @model_class.paper_trail_options[:on] << :destroy
+ append_option_uniquely(:on, :destroy)
end
# Adds a callback that records a version after an "update" event.
@@ -92,19 +77,27 @@ module PaperTrail
@model_class.after_update { |r|
r.paper_trail.clear_version_instance
}
- return if @model_class.paper_trail_options[:on].include?(:update)
- @model_class.paper_trail_options[:on] << :update
+ append_option_uniquely(:on, :update)
end
# Adds a callback that records a version after a "touch" event.
+ #
+ # Rails < 6.0 has a bug where dirty-tracking does not occur during
+ # a `touch`. (https://github.com/rails/rails/issues/33429) See also:
+ # https://github.com/paper-trail-gem/paper_trail/issues/1121
+ # https://github.com/paper-trail-gem/paper_trail/issues/1161
+ # https://github.com/paper-trail-gem/paper_trail/pull/1285
+ #
# @api public
def on_touch
@model_class.after_touch { |r|
- r.paper_trail.record_update(
- force: true,
- in_after_callback: true,
- is_touch: true
- )
+ if r.paper_trail.save_version?
+ r.paper_trail.record_update(
+ force: false,
+ in_after_callback: true,
+ is_touch: true
+ )
+ end
}
end
@@ -117,7 +110,6 @@ module PaperTrail
@model_class.send :include, ::PaperTrail::Model::InstanceMethods
setup_options(options)
setup_associations(options)
- check_presence_of_item_subtype_column(options)
@model_class.after_rollback { paper_trail.clear_rolled_back_versions }
setup_callbacks_from_options options[:on]
end
@@ -129,26 +121,34 @@ module PaperTrail
private
+ # @api private
+ def append_option_uniquely(option, value)
+ collection = @model_class.paper_trail_options.fetch(option)
+ return if collection.include?(value)
+ collection << value
+ end
+
# Raises an error if the provided class is an `abstract_class`.
# @api private
def assert_concrete_activerecord_class(class_name)
if class_name.constantize.abstract_class?
- raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
+ raise Error, format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
end
end
- def cannot_record_after_destroy?
- ::ActiveRecord::Base.belongs_to_required_by_default
+ # @api private
+ def assert_valid_recording_order_for_on_destroy(recording_order)
+ unless %w[after before].include?(recording_order.to_s)
+ raise ArgumentError, 'recording order can only be "after" or "before"'
+ end
+
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
+ raise Error, E_CANNOT_RECORD_AFTER_DESTROY
+ end
end
- # Some options require the presence of the `item_subtype` column. Currently
- # only `limit`, but in the future there may be others.
- #
- # @api private
- def check_presence_of_item_subtype_column(options)
- return unless options.key?(:limit)
- return if version_class.item_subtype_column_present?
- raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name)
+ def cannot_record_after_destroy?
+ ::ActiveRecord::Base.belongs_to_required_by_default
end
def check_version_class_name(options)
@@ -206,6 +206,14 @@ module PaperTrail
options
end
+ # Process an `ignore`, `skip`, or `only` option.
+ def event_attribute_option(option_name)
+ [@model_class.paper_trail_options[option_name]].
+ flatten.
+ compact.
+ map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
+ end
+
def get_versions_scope(options)
options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
end
@@ -240,12 +248,8 @@ module PaperTrail
@model_class.paper_trail_options = options.dup
%i[ignore skip only].each do |k|
- @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
- flatten.
- compact.
- map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
+ @model_class.paper_trail_options[k] = event_attribute_option(k)
end
-
@model_class.paper_trail_options[:meta] ||= {}
end
end
diff --git a/lib/paper_trail/queries/versions/where_attribute_changes.rb b/lib/paper_trail/queries/versions/where_attribute_changes.rb
new file mode 100644
index 0000000..6c87590
--- /dev/null
+++ b/lib/paper_trail/queries/versions/where_attribute_changes.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module PaperTrail
+ module Queries
+ module Versions
+ # For public API documentation, see `where_attribute_changes` in
+ # `paper_trail/version_concern.rb`.
+ # @api private
+ class WhereAttributeChanges
+ # - version_model_class - The class that VersionConcern was mixed into.
+ # - attribute - An attribute that changed. See the public API
+ # documentation for details.
+ # @api private
+ def initialize(version_model_class, attribute)
+ @version_model_class = version_model_class
+ @attribute = attribute
+ end
+
+ # @api private
+ def execute
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_attribute_changes)
+ return PaperTrail.config.object_changes_adapter.where_attribute_changes(
+ @version_model_class, @attribute
+ )
+ end
+ column_type = @version_model_class.columns_hash["object_changes"].type
+ case column_type
+ when :jsonb, :json
+ json
+ else
+ raise UnsupportedColumnType.new(
+ method: "where_attribute_changes",
+ expected: "json or jsonb",
+ actual: column_type
+ )
+ end
+ end
+
+ private
+
+ # @api private
+ def json
+ sql = "object_changes -> ? IS NOT NULL"
+
+ @version_model_class.where(sql, @attribute)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/paper_trail/queries/versions/where_object.rb b/lib/paper_trail/queries/versions/where_object.rb
index 7b277da..9fa6319 100644
--- a/lib/paper_trail/queries/versions/where_object.rb
+++ b/lib/paper_trail/queries/versions/where_object.rb
@@ -19,7 +19,7 @@ module PaperTrail
# @api private
def execute
column = @version_model_class.columns_hash["object"]
- raise "where_object can't be called without an object column" unless column
+ raise Error, "where_object requires an object column" unless column
case column.type
when :jsonb
diff --git a/lib/paper_trail/queries/versions/where_object_changes.rb b/lib/paper_trail/queries/versions/where_object_changes.rb
index d0de325..5ece2cb 100644
--- a/lib/paper_trail/queries/versions/where_object_changes.rb
+++ b/lib/paper_trail/queries/versions/where_object_changes.rb
@@ -15,7 +15,7 @@ module PaperTrail
@version_model_class = version_model_class
# Currently, this `deep_dup` is necessary because the `jsonb` branch
- # modifies `@attributes`, and that would be a nasty suprise for
+ # modifies `@attributes`, and that would be a nasty surprise for
# consumers of this class.
# TODO: Stop modifying `@attributes`, then remove `deep_dup`.
@attributes = attributes.deep_dup
@@ -28,13 +28,18 @@ module PaperTrail
@version_model_class, @attributes
)
end
- case @version_model_class.columns_hash["object_changes"].type
+ column_type = @version_model_class.columns_hash["object_changes"].type
+ case column_type
when :jsonb
jsonb
when :json
json
else
- text
+ raise UnsupportedColumnType.new(
+ method: "where_object_changes",
+ expected: "json or jsonb",
+ actual: column_type
+ )
end
end
@@ -59,16 +64,6 @@ module PaperTrail
@attributes.each { |field, value| @attributes[field] = [value] }
@version_model_class.where("object_changes @> ?", @attributes.to_json)
end
-
- # @api private
- def text
- arel_field = @version_model_class.arel_table[:object_changes]
- where_conditions = @attributes.map { |field, value|
- ::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
- }
- where_conditions = where_conditions.reduce { |a, e| a.and(e) }
- @version_model_class.where(where_conditions)
- end
end
end
end
diff --git a/lib/paper_trail/queries/versions/where_object_changes_from.rb b/lib/paper_trail/queries/versions/where_object_changes_from.rb
index e3d817b..13aaf09 100644
--- a/lib/paper_trail/queries/versions/where_object_changes_from.rb
+++ b/lib/paper_trail/queries/versions/where_object_changes_from.rb
@@ -23,12 +23,16 @@ module PaperTrail
@version_model_class, @attributes
)
end
-
- case @version_model_class.columns_hash["object_changes"].type
+ column_type = @version_model_class.columns_hash["object_changes"].type
+ case column_type
when :jsonb, :json
json
else
- text
+ raise UnsupportedColumnType.new(
+ method: "where_object_changes_from",
+ expected: "json or jsonb",
+ actual: column_type
+ )
end
end
@@ -47,18 +51,6 @@ module PaperTrail
sql = predicates.join(" and ")
@version_model_class.where(sql, *values)
end
-
- # @api private
- def text
- arel_field = @version_model_class.arel_table[:object_changes]
-
- where_conditions = @attributes.map do |field, value|
- ::PaperTrail.serializer.where_object_changes_from_condition(arel_field, field, value)
- end
-
- where_conditions = where_conditions.reduce { |a, e| a.and(e) }
- @version_model_class.where(where_conditions)
- end
end
end
end
diff --git a/lib/paper_trail/queries/versions/where_object_changes_to.rb b/lib/paper_trail/queries/versions/where_object_changes_to.rb
new file mode 100644
index 0000000..2c9a09b
--- /dev/null
+++ b/lib/paper_trail/queries/versions/where_object_changes_to.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module PaperTrail
+ module Queries
+ module Versions
+ # For public API documentation, see `where_object_changes_to` in
+ # `paper_trail/version_concern.rb`.
+ # @api private
+ class WhereObjectChangesTo
+ # - version_model_class - The class that VersionConcern was mixed into.
+ # - attributes - A `Hash` of attributes and values. See the public API
+ # documentation for details.
+ # @api private
+ def initialize(version_model_class, attributes)
+ @version_model_class = version_model_class
+ @attributes = attributes
+ end
+
+ # @api private
+ def execute
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes_to)
+ return PaperTrail.config.object_changes_adapter.where_object_changes_to(
+ @version_model_class, @attributes
+ )
+ end
+ column_type = @version_model_class.columns_hash["object_changes"].type
+ case column_type
+ when :jsonb, :json
+ json
+ else
+ raise UnsupportedColumnType.new(
+ method: "where_object_changes_to",
+ expected: "json or jsonb",
+ actual: column_type
+ )
+ end
+ end
+
+ private
+
+ # @api private
+ def json
+ predicates = []
+ values = []
+ @attributes.each do |field, value|
+ predicates.push(
+ "(object_changes->>? ILIKE ?)"
+ )
+ values.concat([field, "[%#{value.to_json}]"])
+ end
+ sql = predicates.join(" and ")
+ @version_model_class.where(sql, *values)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/paper_trail/record_trail.rb b/lib/paper_trail/record_trail.rb
index 26708f2..8063b70 100644
--- a/lib/paper_trail/record_trail.rb
+++ b/lib/paper_trail/record_trail.rb
@@ -25,14 +25,6 @@ module PaperTrail
@record.send("#{@record.class.version_association_name}=", nil)
end
- # Is PT enabled for this particular record?
- # @api private
- def enabled?
- PaperTrail.enabled? &&
- PaperTrail.request.enabled? &&
- PaperTrail.request.enabled_for_model?(@record.class)
- end
-
# Returns true if this instance is the current, live one;
# returns false if this instance came from a previous version.
def live?
@@ -75,13 +67,6 @@ module PaperTrail
end
end
- # PT-AT extends this method to add its transaction id.
- #
- # @api private
- def data_for_create
- {}
- end
-
# `recording_order` is "after" or "before". See ModelConfig#on_destroy.
#
# @api private
@@ -105,14 +90,12 @@ module PaperTrail
end
end
- # PT-AT extends this method to add its transaction id.
- #
- # @api private
- def data_for_destroy
- {}
- end
-
# @api private
+ # @param force [boolean] Insert a `Version` even if `@record` has not
+ # `changed_notably?`.
+ # @param in_after_callback [boolean] True when called from an `after_update`
+ # or `after_touch` callback.
+ # @param is_touch [boolean] True when called from an `after_touch` callback.
# @return - The created version object, so that plugins can use it, e.g.
# paper_trail-association_tracking
def record_update(force:, in_after_callback:, is_touch:)
@@ -136,40 +119,6 @@ module PaperTrail
end
end
- # PT-AT extends this method to add its transaction id.
- #
- # @api private
- def data_for_update
- {}
- end
-
- # @api private
- # @return - The created version object, so that plugins can use it, e.g.
- # paper_trail-association_tracking
- def record_update_columns(changes)
- return unless enabled?
- event = Events::Update.new(@record, false, false, changes)
-
- # Merge data from `Event` with data from PT-AT. We no longer use
- # `data_for_update_columns` but PT-AT still does.
- data = event.data.merge(data_for_update_columns)
-
- versions_assoc = @record.send(@record.class.versions_association_name)
- version = versions_assoc.create(data)
- if version.errors.any?
- log_version_errors(version, :update)
- else
- version
- end
- end
-
- # PT-AT extends this method to add its transaction id.
- #
- # @api private
- def data_for_update_columns
- {}
- end
-
# Invoked via callback when a user attempts to persist a reified
# `Version`.
def reset_timestamp_attrs_for_update_if_needed
@@ -194,15 +143,17 @@ module PaperTrail
# Save, and create a version record regardless of options such as `:on`,
# `:if`, or `:unless`.
#
- # Arguments are passed to `save`.
+ # `in_after_callback`: Indicates if this method is being called within an
+ # `after` callback. Defaults to `false`.
+ # `options`: Optional arguments passed to `save`.
#
# This is an "update" event. That is, we record the same data we would in
# the case of a normal AR `update`.
- def save_with_version(**options)
+ def save_with_version(in_after_callback: false, **options)
::PaperTrail.request(enabled: false) do
@record.save(**options)
end
- record_update(force: true, in_after_callback: false, is_touch: false)
+ record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
end
# Like the `update_column` method from `ActiveRecord::Persistence`, but also
@@ -267,11 +218,22 @@ module PaperTrail
def build_version_on_update(force:, in_after_callback:, is_touch:)
event = Events::Update.new(@record, in_after_callback, is_touch, nil)
return unless force || event.changed_notably?
+ data = event.data
+
+ # Copy the (recently set) `updated_at` from the record to the `created_at`
+ # of the `Version`. Without this feature, these two timestamps would
+ # differ by a few milliseconds. To some people, it seems a little
+ # unnatural to tamper with creation timestamps in this way. But, this
+ # feature has existed for a long time, almost a decade now, and some users
+ # may rely on it now.
+ if @record.respond_to?(:updated_at)
+ data[:created_at] = @record.updated_at
+ end
# Merge data from `Event` with data from PT-AT. We no longer use
# `data_for_update` but PT-AT still does. To save memory, we use `merge!`
# instead of `merge`.
- data = event.data.merge!(data_for_update)
+ data.merge!(data_for_update)
# Using `version_class.new` reduces memory usage compared to
# `versions_assoc.build`. It's a trade-off though. We have to clear
@@ -280,13 +242,69 @@ module PaperTrail
@record.class.paper_trail.version_class.new(data)
end
+ # PT-AT extends this method to add its transaction id.
+ #
+ # @api public
+ def data_for_create
+ {}
+ end
+
+ # PT-AT extends this method to add its transaction id.
+ #
+ # @api public
+ def data_for_destroy
+ {}
+ end
+
+ # PT-AT extends this method to add its transaction id.
+ #
+ # @api public
+ def data_for_update
+ {}
+ end
+
+ # PT-AT extends this method to add its transaction id.
+ #
+ # @api public
+ def data_for_update_columns
+ {}
+ end
+
+ # Is PT enabled for this particular record?
+ # @api private
+ def enabled?
+ PaperTrail.enabled? &&
+ PaperTrail.request.enabled? &&
+ PaperTrail.request.enabled_for_model?(@record.class)
+ end
+
def log_version_errors(version, action)
version.logger&.warn(
"Unable to create version for #{action} of #{@record.class.name}" \
- "##{@record.id}: " + version.errors.full_messages.join(", ")
+ "##{@record.id}: " + version.errors.full_messages.join(", ")
)
end
+ # @api private
+ # @return - The created version object, so that plugins can use it, e.g.
+ # paper_trail-association_tracking
+ def record_update_columns(changes)
+ return unless enabled?
+ data = Events::Update.new(@record, false, false, changes).data
+
+ # Merge data from `Event` with data from PT-AT. We no longer use
+ # `data_for_update_columns` but PT-AT still does.
+ data.merge!(data_for_update_columns)
+
+ versions_assoc = @record.send(@record.class.versions_association_name)
+ version = versions_assoc.create(data)
+ if version.errors.any?
+ log_version_errors(version, :update)
+ else
+ version
+ end
+ end
+
def version
@record.public_send(@record.class.version_association_name)
end
diff --git a/lib/paper_trail/reifier.rb b/lib/paper_trail/reifier.rb
index fbb587f..d78f872 100644
--- a/lib/paper_trail/reifier.rb
+++ b/lib/paper_trail/reifier.rb
@@ -60,9 +60,7 @@ module PaperTrail
model = if options[:dup] == true || version.event == "destroy"
klass.new
else
- find_cond = { klass.primary_key => version.item_id }
-
- version.item || klass.unscoped.where(find_cond).first || klass.new
+ version.item || init_model_by_finding_item_id(klass, version) || klass.new
end
if options[:unversioned_attributes] == :nil && !model.new_record?
@@ -72,6 +70,11 @@ module PaperTrail
model
end
+ # @api private
+ def init_model_by_finding_item_id(klass, version)
+ klass.unscoped.where(klass.primary_key => version.item_id).first
+ end
+
# Look for attributes that exist in `model` and not in this version.
# These attributes should be set to nil. Modifies `attrs`.
# @api private
@@ -109,21 +112,35 @@ module PaperTrail
end
# Given a `version`, return the class to reify. This method supports
- # Single Table Inheritance (STI) with custom inheritance columns.
+ # Single Table Inheritance (STI) with custom inheritance columns and
+ # custom inheritance column values.
#
# For example, imagine a `version` whose `item_type` is "Animal". The
# `animals` table is an STI table (it has cats and dogs) and it has a
# custom inheritance column, `species`. If `attrs["species"]` is "Dog",
# this method returns the constant `Dog`. If `attrs["species"]` is blank,
- # this method returns the constant `Animal`. You can see this particular
- # example in action in `spec/models/animal_spec.rb`.
+ # this method returns the constant `Animal`.
#
- # TODO: Duplication: similar `constantize` in VersionConcern#version_limit
+ # The values contained in the inheritance columns may be non-camelized
+ # strings (e.g. 'dog' instead of 'Dog'). To reify classes in this case
+ # we need to call the parents class `sti_class_for` method to retrieve
+ # the correct record class.
+ #
+ # You can see these particular examples in action in
+ # `spec/models/animal_spec.rb` and `spec/models/plant_spec.rb`
def version_reification_class(version, attrs)
- inheritance_column_name = version.item_type.constantize.inheritance_column
+ clazz = version.item_type.constantize
+ inheritance_column_name = clazz.inheritance_column
inher_col_value = attrs[inheritance_column_name]
- class_name = inher_col_value.blank? ? version.item_type : inher_col_value
- class_name.constantize
+ return clazz if inher_col_value.blank?
+
+ # Rails 6.1 adds a public method for clients to use to customize STI classes. If that
+ # method is not available, fall back to using the private one
+ if clazz.public_methods.include?(:sti_class_for)
+ return clazz.sti_class_for(inher_col_value)
+ end
+
+ clazz.send(:find_sti_class, inher_col_value)
end
end
end
diff --git a/lib/paper_trail/request.rb b/lib/paper_trail/request.rb
index eb713c4..dbc4e87 100644
--- a/lib/paper_trail/request.rb
+++ b/lib/paper_trail/request.rb
@@ -12,9 +12,6 @@ module PaperTrail
#
# @api private
module Request
- class InvalidOption < RuntimeError
- end
-
class << self
# Sets any data from the controller that you want PaperTrail to store.
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
@@ -78,28 +75,6 @@ module PaperTrail
!!store.fetch(:"enabled_for_#{model}", true)
end
- # @api private
- def merge(options)
- options.to_h.each do |k, v|
- store[k] = v
- end
- end
-
- # @api private
- def set(options)
- store.clear
- merge(options)
- end
-
- # Returns a deep copy of the internal hash from our RequestStore. Keys are
- # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
- # We cannot use Marshal.dump here because it doesn't support Proc. It is
- # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
- # @api private
- def to_h
- store.deep_dup
- end
-
# Temporarily set `options` and execute a block.
# @api private
def with(options)
@@ -136,6 +111,19 @@ module PaperTrail
private
+ # @api private
+ def merge(options)
+ options.to_h.each do |k, v|
+ store[k] = v
+ end
+ end
+
+ # @api private
+ def set(options)
+ store.clear
+ merge(options)
+ end
+
# Returns a Hash, initializing with default values if necessary.
# @api private
def store
@@ -144,6 +132,15 @@ module PaperTrail
}
end
+ # Returns a deep copy of the internal hash from our RequestStore. Keys are
+ # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
+ # We cannot use Marshal.dump here because it doesn't support Proc. It is
+ # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
+ # @api private
+ def to_h
+ store.deep_dup
+ end
+
# Provide a helpful error message if someone has a typo in one of their
# option keys. We don't validate option values here. That's traditionally
# been handled with casting (`to_s`, `!!`) in the accessor method.
diff --git a/lib/paper_trail/serializers/json.rb b/lib/paper_trail/serializers/json.rb
index 0a58f3e..f0cbf89 100644
--- a/lib/paper_trail/serializers/json.rb
+++ b/lib/paper_trail/serializers/json.rb
@@ -31,24 +31,6 @@ module PaperTrail
arel_field.matches("%\"#{field}\":#{json_value}%")
end
end
-
- def where_object_changes_condition(*)
- raise <<-STR.squish.freeze
- where_object_changes no longer supports reading JSON from a text
- column. The old implementation was inaccurate, returning more records
- than you wanted. This feature was deprecated in 7.1.0 and removed in
- 8.0.0. The json and jsonb datatypes are still supported. See the
- discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
- STR
- end
-
- # Raises an exception as this operation is not allowed from text columns.
- def where_object_changes_from_condition(*)
- raise <<-STR.squish.freeze
- where_object_changes_from does not support reading JSON from a text
- column. The json and jsonb datatypes are supported.
- STR
- end
end
end
end
diff --git a/lib/paper_trail/serializers/yaml.rb b/lib/paper_trail/serializers/yaml.rb
index 666b4dd..9e6bc03 100644
--- a/lib/paper_trail/serializers/yaml.rb
+++ b/lib/paper_trail/serializers/yaml.rb
@@ -9,13 +9,23 @@ module PaperTrail
extend self # makes all instance methods become module methods as well
def load(string)
- ::YAML.load string
+ if use_safe_load?
+ ::YAML.safe_load(
+ string,
+ permitted_classes: yaml_column_permitted_classes,
+ aliases: true
+ )
+ elsif ::YAML.respond_to?(:unsafe_load)
+ ::YAML.unsafe_load(string)
+ else
+ ::YAML.load(string)
+ end
end
# @param object (Hash | HashWithIndifferentAccess) - Coming from
# `recordable_object` `object` will be a plain `Hash`. However, due to
- # recent [memory optimizations](https://git.io/fjeYv), when coming from
- # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
+ # recent [memory optimizations](https://github.com/paper-trail-gem/paper_trail/pull/1189),
+ # when coming from `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
def dump(object)
object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
::YAML.dump object
@@ -27,24 +37,31 @@ module PaperTrail
arel_field.matches("%\n#{field}: #{value}\n%")
end
- # Returns a SQL LIKE condition to be used to match the given field and
- # value in the serialized `object_changes`.
- def where_object_changes_condition(*)
- raise <<-STR.squish.freeze
- where_object_changes no longer supports reading YAML from a text
- column. The old implementation was inaccurate, returning more records
- than you wanted. This feature was deprecated in 8.1.0 and removed in
- 9.0.0. The json and jsonb datatypes are still supported. See
- discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
- STR
+ private
+
+ def use_safe_load?
+ if ::ActiveRecord.gem_version >= Gem::Version.new("7.0.3.1")
+ # `use_yaml_unsafe_load` may be removed in the future, at which point
+ # safe loading will be the default.
+ !defined?(ActiveRecord.use_yaml_unsafe_load) || !ActiveRecord.use_yaml_unsafe_load
+ elsif defined?(ActiveRecord::Base.use_yaml_unsafe_load)
+ # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
+ !ActiveRecord::Base.use_yaml_unsafe_load
+ else
+ false
+ end
end
- # Raises an exception as this operation is not allowed with YAML.
- def where_object_changes_from_condition(*)
- raise <<-STR.squish.freeze
- where_object_changes_from does not support reading YAML from a text
- column. The json and jsonb datatypes are supported.
- STR
+ def yaml_column_permitted_classes
+ if defined?(ActiveRecord.yaml_column_permitted_classes)
+ # Rails >= 7.0.3.1
+ ActiveRecord.yaml_column_permitted_classes
+ elsif defined?(ActiveRecord::Base.yaml_column_permitted_classes)
+ # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
+ ActiveRecord::Base.yaml_column_permitted_classes
+ else
+ []
+ end
end
end
end
diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb
index 429ac60..c49bfe0 100644
--- a/lib/paper_trail/version_concern.rb
+++ b/lib/paper_trail/version_concern.rb
@@ -1,9 +1,11 @@
# frozen_string_literal: true
require "paper_trail/attribute_serializers/object_changes_attribute"
+require "paper_trail/queries/versions/where_attribute_changes"
require "paper_trail/queries/versions/where_object"
require "paper_trail/queries/versions/where_object_changes"
require "paper_trail/queries/versions/where_object_changes_from"
+require "paper_trail/queries/versions/where_object_changes_to"
module PaperTrail
# Originally, PaperTrail did not provide this module, and all of this
@@ -13,18 +15,20 @@ module PaperTrail
module VersionConcern
extend ::ActiveSupport::Concern
+ E_YAML_PERMITTED_CLASSES = <<-EOS.squish.freeze
+ PaperTrail encountered a Psych::DisallowedClass error during
+ deserialization of YAML column, indicating that
+ yaml_column_permitted_classes has not been configured correctly. %s
+ EOS
+
included do
- belongs_to :item, polymorphic: true, optional: true
+ belongs_to :item, polymorphic: true, optional: true, inverse_of: false
validates_presence_of :event
after_create :enforce_version_limit!
end
# :nodoc:
module ClassMethods
- def item_subtype_column_present?
- column_names.include?("item_subtype")
- end
-
def with_item_keys(item_type, item_id)
where item_type: item_type, item_id: item_id
end
@@ -42,7 +46,7 @@ module PaperTrail
end
def not_creates
- where "event <> ?", "create"
+ where.not(event: "create")
end
def between(start_time, end_time)
@@ -60,6 +64,18 @@ module PaperTrail
end
end
+ # Given an attribute like `"name"`, query the `versions.object_changes`
+ # column for any changes that modified the provided attribute.
+ #
+ # @api public
+ def where_attribute_changes(attribute)
+ unless attribute.is_a?(String) || attribute.is_a?(Symbol)
+ raise ArgumentError, "expected to receive a String or Symbol"
+ end
+
+ Queries::Versions::WhereAttributeChanges.new(self, attribute).execute
+ end
+
# Given a hash of attributes like `name: 'Joan'`, query the
# `versions.objects` column.
#
@@ -131,6 +147,21 @@ module PaperTrail
Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
end
+ # Given a hash of attributes like `name: 'Joan'`, query the
+ # `versions.objects_changes` column for changes where the version changed
+ # to the hash of attributes from other values.
+ #
+ # This is useful for finding versions where the attribute started with an
+ # unknown value and changed to a known value. This is in comparison to
+ # `where_object_changes` which will find both the changes before and
+ # after.
+ #
+ # @api public
+ def where_object_changes_to(args = {})
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
+ Queries::Versions::WhereObjectChangesTo.new(self, args).execute
+ end
+
def primary_key_is_int?
@primary_key_is_int ||= columns_hash[primary_key].type == :integer
rescue StandardError # TODO: Rescue something more specific
@@ -237,7 +268,7 @@ module PaperTrail
#
def reify(options = {})
unless self.class.column_names.include? "object"
- raise "reify can't be called without an object column"
+ raise Error, "reify requires an object column"
end
return nil if object.nil?
::PaperTrail::Reifier.reify(self, options)
@@ -323,7 +354,10 @@ module PaperTrail
else
begin
PaperTrail.serializer.load(object_changes)
- rescue StandardError # TODO: Rescue something more specific
+ rescue StandardError => e
+ if defined?(::Psych::Exception) && e.instance_of?(::Psych::Exception)
+ ::Kernel.warn format(E_YAML_PERMITTED_CLASSES, e)
+ end
{}
end
end
@@ -350,16 +384,23 @@ module PaperTrail
# The version limit can be global or per-model.
#
# @api private
- #
- # TODO: Duplication: similar `constantize` in Reifier#version_reification_class
def version_limit
- if self.class.item_subtype_column_present?
- klass = (item_subtype || item_type).constantize
- if klass&.paper_trail_options&.key?(:limit)
- return klass.paper_trail_options[:limit]
- end
+ klass = item.class
+ if limit_option?(klass)
+ klass.paper_trail_options[:limit]
+ elsif base_class_limit_option?(klass)
+ klass.base_class.paper_trail_options[:limit]
+ else
+ PaperTrail.config.version_limit
end
- PaperTrail.config.version_limit
+ end
+
+ def limit_option?(klass)
+ klass.respond_to?(:paper_trail_options) && klass.paper_trail_options.key?(:limit)
+ end
+
+ def base_class_limit_option?(klass)
+ klass.respond_to?(:base_class) && limit_option?(klass.base_class)
end
end
end
diff --git a/lib/paper_trail/version_number.rb b/lib/paper_trail/version_number.rb
index cf2cfae..1f8f3ea 100644
--- a/lib/paper_trail/version_number.rb
+++ b/lib/paper_trail/version_number.rb
@@ -7,7 +7,7 @@ module PaperTrail
# because of this confusion, but it's not worth the breaking change.
# People are encouraged to use `PaperTrail.gem_version` instead.
module VERSION
- MAJOR = 12
+ MAJOR = 14
MINOR = 0
TINY = 0
diff --git a/paper_trail.gemspec b/paper_trail.gemspec
index c67c5aa..0ea8b65 100644
--- a/paper_trail.gemspec
+++ b/paper_trail.gemspec
@@ -43,20 +43,20 @@ has been destroyed.
# about 3 years, per https://www.ruby-lang.org/en/downloads/branches/
#
# See "Lowest supported ruby version" in CONTRIBUTING.md
- #
- # Ruby 2.5 reaches EoL on 2021-03-31.
- s.required_ruby_version = ">= 2.5.0"
+ s.required_ruby_version = ">= 2.7.0"
# We no longer specify a maximum activerecord version.
# See discussion in paper_trail/compatibility.rb
s.add_dependency "activerecord", ::PaperTrail::Compatibility::ACTIVERECORD_GTE
- s.add_dependency "request_store", "~> 1.1"
- s.add_development_dependency "appraisal", "~> 2.2"
- s.add_development_dependency "byebug", "~> 11.0"
- s.add_development_dependency "ffaker", "~> 2.11"
+ # PT supports request_store versions for 3 years.
+ s.add_dependency "request_store", "~> 1.4"
+
+ s.add_development_dependency "appraisal", "~> 2.4.1"
+ s.add_development_dependency "byebug", "~> 11.1"
+ s.add_development_dependency "ffaker", "~> 2.20"
s.add_development_dependency "generator_spec", "~> 0.9.4"
- s.add_development_dependency "memory_profiler", "~> 0.9.14"
+ s.add_development_dependency "memory_profiler", "~> 1.0.0"
# For `spec/dummy_app`. Technically, we only need `actionpack` (as of 2020).
# However, that might change in the future, and the advantages of specifying a
@@ -64,12 +64,14 @@ has been destroyed.
s.add_development_dependency "rails", ::PaperTrail::Compatibility::ACTIVERECORD_GTE
s.add_development_dependency "rake", "~> 13.0"
- s.add_development_dependency "rspec-rails", "~> 4.0"
- s.add_development_dependency "rubocop", "~> 1.11.0"
- s.add_development_dependency "rubocop-rails", "~> 2.9.1"
+ s.add_development_dependency "rspec-rails", "~> 5.0.2"
+ s.add_development_dependency "rubocop", "~> 1.22.2"
s.add_development_dependency "rubocop-packaging", "~> 0.5.1"
- s.add_development_dependency "rubocop-performance", "~> 1.10.1"
- s.add_development_dependency "rubocop-rspec", "~> 2.2.0"
+ s.add_development_dependency "rubocop-performance", "~> 1.11.5"
+ s.add_development_dependency "rubocop-rails", "~> 2.12.4"
+ s.add_development_dependency "rubocop-rake", "~> 0.6.0"
+ s.add_development_dependency "rubocop-rspec", "~> 2.5.0"
+ s.add_development_dependency "simplecov", "~> 0.21.2"
# ## Database Adapters
#
@@ -81,7 +83,7 @@ has been destroyed.
# Currently, all versions of rails we test against are consistent. In the past,
# when we tested against rails 4.2, we had to specify database adapters in
# `Appraisals`.
- s.add_development_dependency "mysql2", "~> 0.5"
- s.add_development_dependency "pg", ">= 0.18", "< 2.0"
+ s.add_development_dependency "mysql2", "~> 0.5.3"
+ s.add_development_dependency "pg", "~> 1.2"
s.add_development_dependency "sqlite3", "~> 1.4"
end
diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb
index dee8c0d..15d11c3 100644
--- a/spec/controllers/articles_controller_spec.rb
+++ b/spec/controllers/articles_controller_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe ArticlesController, type: :controller do
describe "PaperTrail.request.enabled?" do
- context "PaperTrail.enabled? == true" do
+ context "when PaperTrail.enabled? == true" do
before { PaperTrail.enabled = true }
after { PaperTrail.enabled = false }
@@ -18,7 +18,7 @@ RSpec.describe ArticlesController, type: :controller do
end
end
- context "PaperTrail.enabled? == false" do
+ context "when PaperTrail.enabled? == false" do
it "returns false" do
expect(PaperTrail.enabled?).to eq(false)
post :create, params: { article: { title: "Doh", content: FFaker::Lorem.sentence } }
diff --git a/spec/controllers/widgets_controller_spec.rb b/spec/controllers/widgets_controller_spec.rb
index 598dca8..ae24ff4 100644
--- a/spec/controllers/widgets_controller_spec.rb
+++ b/spec/controllers/widgets_controller_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe WidgetsController, type: :controller, versioning: true do
after { RequestStore.store[:paper_trail] = nil }
describe "#create" do
- context "PT enabled" do
+ context "with PT enabled" do
it "stores information like IP address in version" do
post(:create, params: { widget: { name: "Flugel" } })
widget = assigns(:widget)
@@ -29,7 +29,7 @@ RSpec.describe WidgetsController, type: :controller, versioning: true do
end
end
- context "PT disabled" do
+ context "with PT disabled" do
it "does not save a version, and metadata is not set" do
request.env["HTTP_USER_AGENT"] = "Disable User-Agent"
post :create, params: { widget: { name: "Flugel" } }
diff --git a/spec/dummy_app/app/controllers/application_controller.rb b/spec/dummy_app/app/controllers/application_controller.rb
index 9283887..bb999f0 100644
--- a/spec/dummy_app/app/controllers/application_controller.rb
+++ b/spec/dummy_app/app/controllers/application_controller.rb
@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
before_action :modify_current_user
# PT used to add this callback automatically. Now people are required to add
- # it themsevles, like this, allowing them to control the order of callbacks.
+ # it themselves, like this, allowing them to control the order of callbacks.
# The `modify_current_user` callback above shows why this control is useful.
before_action :set_paper_trail_whodunnit
diff --git a/spec/dummy_app/app/controllers/test_controller.rb b/spec/dummy_app/app/controllers/test_controller.rb
index da41c69..04a6681 100644
--- a/spec/dummy_app/app/controllers/test_controller.rb
+++ b/spec/dummy_app/app/controllers/test_controller.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class TestController < ActionController::Base
+class TestController < ApplicationController
def user_for_paper_trail
Thread.current.object_id
end
diff --git a/spec/dummy_app/app/models/animal.rb b/spec/dummy_app/app/models/animal.rb
index 748c68f..4b262f9 100644
--- a/spec/dummy_app/app/models/animal.rb
+++ b/spec/dummy_app/app/models/animal.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Animal < ActiveRecord::Base
+class Animal < ApplicationRecord
has_paper_trail
self.inheritance_column = "species"
end
diff --git a/spec/dummy_app/app/models/application_record.rb b/spec/dummy_app/app/models/application_record.rb
new file mode 100644
index 0000000..71fbba5
--- /dev/null
+++ b/spec/dummy_app/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/spec/dummy_app/app/models/article.rb b/spec/dummy_app/app/models/article.rb
index 8096c89..a809bdf 100644
--- a/spec/dummy_app/app/models/article.rb
+++ b/spec/dummy_app/app/models/article.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# Demonstrates the `only` and `ignore` attributes, among other things.
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
has_paper_trail(
ignore: [
:title, {
diff --git a/spec/dummy_app/app/models/authorship.rb b/spec/dummy_app/app/models/authorship.rb
index d56dd15..33fc1ed 100644
--- a/spec/dummy_app/app/models/authorship.rb
+++ b/spec/dummy_app/app/models/authorship.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Authorship < ActiveRecord::Base
+class Authorship < ApplicationRecord
belongs_to :book
belongs_to :author, class_name: "Person"
has_paper_trail
diff --git a/spec/dummy_app/app/models/bar_habtm.rb b/spec/dummy_app/app/models/bar_habtm.rb
index 5cd81fa..4083fac 100644
--- a/spec/dummy_app/app/models/bar_habtm.rb
+++ b/spec/dummy_app/app/models/bar_habtm.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class BarHabtm < ActiveRecord::Base
+class BarHabtm < ApplicationRecord
has_and_belongs_to_many :foo_habtms
has_paper_trail
end
diff --git a/spec/dummy_app/app/models/book.rb b/spec/dummy_app/app/models/book.rb
index f697a83..196147f 100644
--- a/spec/dummy_app/app/models/book.rb
+++ b/spec/dummy_app/app/models/book.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Book < ActiveRecord::Base
+class Book < ApplicationRecord
has_many :authorships, dependent: :destroy
has_many :authors, through: :authorships
diff --git a/spec/dummy_app/app/models/boolit.rb b/spec/dummy_app/app/models/boolit.rb
index 28bed19..ec5cb55 100644
--- a/spec/dummy_app/app/models/boolit.rb
+++ b/spec/dummy_app/app/models/boolit.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Boolit < ActiveRecord::Base
+class Boolit < ApplicationRecord
default_scope { where(scoped: true) }
has_paper_trail
end
diff --git a/spec/dummy_app/app/models/callback_modifier.rb b/spec/dummy_app/app/models/callback_modifier.rb
index dc33217..a287f53 100644
--- a/spec/dummy_app/app/models/callback_modifier.rb
+++ b/spec/dummy_app/app/models/callback_modifier.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class CallbackModifier < ActiveRecord::Base
+class CallbackModifier < ApplicationRecord
has_paper_trail on: []
def test_destroy
diff --git a/spec/dummy_app/app/models/car.rb b/spec/dummy_app/app/models/car.rb
index ea3faa5..a6064a8 100644
--- a/spec/dummy_app/app/models/car.rb
+++ b/spec/dummy_app/app/models/car.rb
@@ -2,4 +2,6 @@
class Car < Vehicle
has_paper_trail
+ attribute :color, type: ActiveModel::Type::String
+ attr_accessor :top_speed
end
diff --git a/spec/dummy_app/app/models/chapter.rb b/spec/dummy_app/app/models/chapter.rb
index 6a0c92f..b0c7ca0 100644
--- a/spec/dummy_app/app/models/chapter.rb
+++ b/spec/dummy_app/app/models/chapter.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Chapter < ActiveRecord::Base
+class Chapter < ApplicationRecord
has_many :sections, dependent: :destroy
has_many :paragraphs, through: :sections
diff --git a/spec/dummy_app/app/models/citation.rb b/spec/dummy_app/app/models/citation.rb
index bf372de..134d9b7 100644
--- a/spec/dummy_app/app/models/citation.rb
+++ b/spec/dummy_app/app/models/citation.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Citation < ActiveRecord::Base
+class Citation < ApplicationRecord
belongs_to :quotation
has_paper_trail
diff --git a/spec/dummy_app/app/models/custom_primary_key_record.rb b/spec/dummy_app/app/models/custom_primary_key_record.rb
index f72f5f3..329ae0b 100644
--- a/spec/dummy_app/app/models/custom_primary_key_record.rb
+++ b/spec/dummy_app/app/models/custom_primary_key_record.rb
@@ -2,7 +2,7 @@
require "securerandom"
-class CustomPrimaryKeyRecord < ActiveRecord::Base
+class CustomPrimaryKeyRecord < ApplicationRecord
self.primary_key = :uuid
has_paper_trail versions: { class_name: "CustomPrimaryKeyRecordVersion" }
diff --git a/spec/dummy_app/app/models/customer.rb b/spec/dummy_app/app/models/customer.rb
index c487104..f73de64 100644
--- a/spec/dummy_app/app/models/customer.rb
+++ b/spec/dummy_app/app/models/customer.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Customer < ActiveRecord::Base
+class Customer < ApplicationRecord
has_many :orders, dependent: :destroy
has_paper_trail
end
diff --git a/spec/dummy_app/app/models/document.rb b/spec/dummy_app/app/models/document.rb
index 2197029..a017dfc 100644
--- a/spec/dummy_app/app/models/document.rb
+++ b/spec/dummy_app/app/models/document.rb
@@ -2,7 +2,7 @@
# Demonstrates a "custom versions association name". Instead of the association
# being named `versions`, it will be named `paper_trail_versions`.
-class Document < ActiveRecord::Base
+class Document < ApplicationRecord
has_paper_trail(
versions: { name: :paper_trail_versions },
on: %i[create update]
diff --git a/spec/dummy_app/app/models/editor.rb b/spec/dummy_app/app/models/editor.rb
index 070f88e..3c810e7 100644
--- a/spec/dummy_app/app/models/editor.rb
+++ b/spec/dummy_app/app/models/editor.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
# to demonstrate a has_through association that does not have paper_trail enabled
-class Editor < ActiveRecord::Base
+class Editor < ApplicationRecord
has_many :editorships, dependent: :destroy
end
diff --git a/spec/dummy_app/app/models/editorship.rb b/spec/dummy_app/app/models/editorship.rb
index aa37fec..588129d 100644
--- a/spec/dummy_app/app/models/editorship.rb
+++ b/spec/dummy_app/app/models/editorship.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Editorship < ActiveRecord::Base
+class Editorship < ApplicationRecord
belongs_to :book
belongs_to :editor
has_paper_trail
diff --git a/spec/dummy_app/app/models/family/family.rb b/spec/dummy_app/app/models/family/family.rb
index f13bbc2..7a1e3ce 100644
--- a/spec/dummy_app/app/models/family/family.rb
+++ b/spec/dummy_app/app/models/family/family.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
module Family
- class Family < ActiveRecord::Base
+ class Family < ApplicationRecord
has_paper_trail
has_many :familie_lines, class_name: "::Family::FamilyLine", foreign_key: :parent_id
has_many :children, class_name: "::Family::Family", foreign_key: :parent_id
has_many :grandsons, through: :familie_lines
has_one :mentee, class_name: "::Family::Family", foreign_key: :partner_id
- belongs_to :parent, class_name: "::Family::Family", foreign_key: :parent_id, optional: true
+ belongs_to :parent, class_name: "::Family::Family", optional: true
belongs_to :mentor, class_name: "::Family::Family", foreign_key: :partner_id, optional: true
accepts_nested_attributes_for :mentee
diff --git a/spec/dummy_app/app/models/family/family_line.rb b/spec/dummy_app/app/models/family/family_line.rb
index c0679b0..93bd4d9 100644
--- a/spec/dummy_app/app/models/family/family_line.rb
+++ b/spec/dummy_app/app/models/family/family_line.rb
@@ -1,15 +1,13 @@
# frozen_string_literal: true
module Family
- class FamilyLine < ActiveRecord::Base
+ class FamilyLine < ApplicationRecord
has_paper_trail
belongs_to :parent,
class_name: "::Family::Family",
- foreign_key: :parent_id,
optional: true
belongs_to :grandson,
class_name: "::Family::Family",
- foreign_key: :grandson_id,
optional: true
end
end
diff --git a/spec/dummy_app/app/models/fluxor.rb b/spec/dummy_app/app/models/fluxor.rb
index f596a61..fe9fe1c 100644
--- a/spec/dummy_app/app/models/fluxor.rb
+++ b/spec/dummy_app/app/models/fluxor.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class Fluxor < ActiveRecord::Base
+class Fluxor < ApplicationRecord
belongs_to :widget, optional: true
end
diff --git a/spec/dummy_app/app/models/foo_habtm.rb b/spec/dummy_app/app/models/foo_habtm.rb
index d101418..7df26fa 100644
--- a/spec/dummy_app/app/models/foo_habtm.rb
+++ b/spec/dummy_app/app/models/foo_habtm.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class FooHabtm < ActiveRecord::Base
+class FooHabtm < ApplicationRecord
has_and_belongs_to_many :bar_habtms
accepts_nested_attributes_for :bar_habtms
has_paper_trail
diff --git a/spec/dummy_app/app/models/fruit.rb b/spec/dummy_app/app/models/fruit.rb
index 1237612..958f1ee 100644
--- a/spec/dummy_app/app/models/fruit.rb
+++ b/spec/dummy_app/app/models/fruit.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-class Fruit < ActiveRecord::Base
- if ENV["DB"] == "postgres" || JsonVersion.table_exists?
+# See also `Vegetable` which uses `JsonbVersion`.
+class Fruit < ApplicationRecord
+ if ENV["DB"] == "postgres"
has_paper_trail versions: { class_name: "JsonVersion" }
end
end
diff --git a/spec/dummy_app/app/models/gadget.rb b/spec/dummy_app/app/models/gadget.rb
index 9a2f663..a845300 100644
--- a/spec/dummy_app/app/models/gadget.rb
+++ b/spec/dummy_app/app/models/gadget.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class Gadget < ActiveRecord::Base
+class Gadget < ApplicationRecord
has_paper_trail ignore: [:brand, { color: proc { |obj| obj.color == "Yellow" } }]
end
diff --git a/spec/dummy_app/app/models/kitchen/banana.rb b/spec/dummy_app/app/models/kitchen/banana.rb
index ed2a4f1..024fbfe 100644
--- a/spec/dummy_app/app/models/kitchen/banana.rb
+++ b/spec/dummy_app/app/models/kitchen/banana.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Kitchen
- class Banana < ActiveRecord::Base
+ class Banana < ApplicationRecord
has_paper_trail versions: { class_name: "Kitchen::BananaVersion" }
end
end
diff --git a/spec/dummy_app/app/models/legacy_widget.rb b/spec/dummy_app/app/models/legacy_widget.rb
index ff2e526..31e24f3 100644
--- a/spec/dummy_app/app/models/legacy_widget.rb
+++ b/spec/dummy_app/app/models/legacy_widget.rb
@@ -3,6 +3,6 @@
# The `legacy_widgets` table has a `version` column that would conflict with our
# `version` method. It is configured to define a method named `custom_version`
# instead.
-class LegacyWidget < ActiveRecord::Base
+class LegacyWidget < ApplicationRecord
has_paper_trail ignore: :version, version: "custom_version"
end
diff --git a/spec/dummy_app/app/models/line_item.rb b/spec/dummy_app/app/models/line_item.rb
index 08b720e..639aa57 100644
--- a/spec/dummy_app/app/models/line_item.rb
+++ b/spec/dummy_app/app/models/line_item.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class LineItem < ActiveRecord::Base
+class LineItem < ApplicationRecord
belongs_to :order, dependent: :destroy
has_paper_trail
end
diff --git a/spec/dummy_app/app/models/no_object.rb b/spec/dummy_app/app/models/no_object.rb
index cf75832..7eb0b87 100644
--- a/spec/dummy_app/app/models/no_object.rb
+++ b/spec/dummy_app/app/models/no_object.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# Demonstrates a table that omits the `object` column.
-class NoObject < ActiveRecord::Base
+class NoObject < ApplicationRecord
has_paper_trail(
versions: { class_name: "NoObjectVersion" },
meta: { metadatum: 42 }
diff --git a/spec/dummy_app/app/models/not_on_update.rb b/spec/dummy_app/app/models/not_on_update.rb
index fc95bd2..f511908 100644
--- a/spec/dummy_app/app/models/not_on_update.rb
+++ b/spec/dummy_app/app/models/not_on_update.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
# This model does not record versions when updated.
-class NotOnUpdate < ActiveRecord::Base
+class NotOnUpdate < ApplicationRecord
has_paper_trail on: %i[create destroy]
end
diff --git a/spec/dummy_app/app/models/on/create.rb b/spec/dummy_app/app/models/on/create.rb
index 626e344..e47c257 100644
--- a/spec/dummy_app/app/models/on/create.rb
+++ b/spec/dummy_app/app/models/on/create.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module On
- class Create < ActiveRecord::Base
+ class Create < ApplicationRecord
self.table_name = :on_create
has_paper_trail on: [:create]
end
diff --git a/spec/dummy_app/app/models/on/destroy.rb b/spec/dummy_app/app/models/on/destroy.rb
index 6fe86d6..485e4be 100644
--- a/spec/dummy_app/app/models/on/destroy.rb
+++ b/spec/dummy_app/app/models/on/destroy.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module On
- class Destroy < ActiveRecord::Base
+ class Destroy < ApplicationRecord
self.table_name = :on_destroy
has_paper_trail on: [:destroy]
end
diff --git a/spec/dummy_app/app/models/on/empty_array.rb b/spec/dummy_app/app/models/on/empty_array.rb
index 0de30e8..1f8f1c5 100644
--- a/spec/dummy_app/app/models/on/empty_array.rb
+++ b/spec/dummy_app/app/models/on/empty_array.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module On
- class EmptyArray < ActiveRecord::Base
+ class EmptyArray < ApplicationRecord
self.table_name = :on_empty_array
has_paper_trail on: []
end
diff --git a/spec/dummy_app/app/models/on/touch.rb b/spec/dummy_app/app/models/on/touch.rb
index 37455c3..c7efd68 100644
--- a/spec/dummy_app/app/models/on/touch.rb
+++ b/spec/dummy_app/app/models/on/touch.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module On
- class Touch < ActiveRecord::Base
+ class Touch < ApplicationRecord
self.table_name = :on_touch
has_paper_trail on: [:touch]
end
diff --git a/spec/dummy_app/app/models/on/update.rb b/spec/dummy_app/app/models/on/update.rb
index 0c48f29..8571694 100644
--- a/spec/dummy_app/app/models/on/update.rb
+++ b/spec/dummy_app/app/models/on/update.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module On
- class Update < ActiveRecord::Base
+ class Update < ApplicationRecord
self.table_name = :on_update
has_paper_trail on: [:update]
end
diff --git a/spec/dummy_app/app/models/order.rb b/spec/dummy_app/app/models/order.rb
index f272874..84e5ab4 100644
--- a/spec/dummy_app/app/models/order.rb
+++ b/spec/dummy_app/app/models/order.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-class Order < ActiveRecord::Base
- belongs_to :customer
+class Order < ApplicationRecord
+ belongs_to :customer, touch: :touched_at
has_many :line_items
has_paper_trail
end
diff --git a/spec/dummy_app/app/models/paragraph.rb b/spec/dummy_app/app/models/paragraph.rb
index b92c7b6..efb05eb 100644
--- a/spec/dummy_app/app/models/paragraph.rb
+++ b/spec/dummy_app/app/models/paragraph.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Paragraph < ActiveRecord::Base
+class Paragraph < ApplicationRecord
belongs_to :section
has_paper_trail
diff --git a/spec/dummy_app/app/models/person.rb b/spec/dummy_app/app/models/person.rb
index 77dca21..fb1aa4b 100644
--- a/spec/dummy_app/app/models/person.rb
+++ b/spec/dummy_app/app/models/person.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
has_many :authorships, foreign_key: :author_id, dependent: :destroy
has_many :books, through: :authorships
@@ -14,7 +14,7 @@ class Person < ActiveRecord::Base
has_one :thing
- belongs_to :mentor, class_name: "Person", foreign_key: :mentor_id, optional: true
+ belongs_to :mentor, class_name: "Person", optional: true
has_paper_trail
diff --git a/spec/dummy_app/app/models/pet.rb b/spec/dummy_app/app/models/pet.rb
index ee9944d..0d7c0b8 100644
--- a/spec/dummy_app/app/models/pet.rb
+++ b/spec/dummy_app/app/models/pet.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Pet < ActiveRecord::Base
+class Pet < ApplicationRecord
belongs_to :owner, class_name: "Person", optional: true
belongs_to :animal, optional: true
has_paper_trail
diff --git a/spec/dummy_app/app/models/plant.rb b/spec/dummy_app/app/models/plant.rb
new file mode 100644
index 0000000..e1d88b5
--- /dev/null
+++ b/spec/dummy_app/app/models/plant.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Plant < ApplicationRecord
+ has_paper_trail
+ self.inheritance_column = "species"
+
+ class << self
+ # Rails 6.1 adds a public method to overwrite sti finder methods. In earlier versions, users
+ # may use the private method find_sti_class.
+ #
+ # See https://github.com/rails/rails/pull/37500
+ if method_defined?(:sti_class_for)
+ def sti_class_for(type_name)
+ super(type_name.camelize)
+ end
+ else
+ def find_sti_class(type_name)
+ super(type_name.camelize)
+ end
+ end
+ end
+end
diff --git a/spec/dummy_app/app/models/post.rb b/spec/dummy_app/app/models/post.rb
index 68871df..39c5738 100644
--- a/spec/dummy_app/app/models/post.rb
+++ b/spec/dummy_app/app/models/post.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class Post < ActiveRecord::Base
+class Post < ApplicationRecord
has_paper_trail versions: { class_name: "PostVersion" }
end
diff --git a/spec/dummy_app/app/models/post_with_status.rb b/spec/dummy_app/app/models/post_with_status.rb
index d9685f9..3a408ad 100644
--- a/spec/dummy_app/app/models/post_with_status.rb
+++ b/spec/dummy_app/app/models/post_with_status.rb
@@ -2,7 +2,7 @@
# This model tests ActiveRecord::Enum, which was added in AR 4.1
# http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
-class PostWithStatus < ActiveRecord::Base
+class PostWithStatus < ApplicationRecord
has_paper_trail
enum status: { draft: 0, published: 1, archived: 2 }
diff --git a/spec/dummy_app/app/models/postgres_user.rb b/spec/dummy_app/app/models/postgres_user.rb
index b6cfea8..c238ae3 100644
--- a/spec/dummy_app/app/models/postgres_user.rb
+++ b/spec/dummy_app/app/models/postgres_user.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class PostgresUser < ActiveRecord::Base
+class PostgresUser < ApplicationRecord
has_paper_trail
end
diff --git a/spec/dummy_app/app/models/quotation.rb b/spec/dummy_app/app/models/quotation.rb
index 1389228..f9ee95a 100644
--- a/spec/dummy_app/app/models/quotation.rb
+++ b/spec/dummy_app/app/models/quotation.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Quotation < ActiveRecord::Base
+class Quotation < ApplicationRecord
belongs_to :chapter
has_many :citations, dependent: :destroy
has_paper_trail
diff --git a/spec/dummy_app/app/models/section.rb b/spec/dummy_app/app/models/section.rb
index 81a1e56..424a71a 100644
--- a/spec/dummy_app/app/models/section.rb
+++ b/spec/dummy_app/app/models/section.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Section < ActiveRecord::Base
+class Section < ApplicationRecord
belongs_to :chapter
has_many :paragraphs, dependent: :destroy
diff --git a/spec/dummy_app/app/models/skipper.rb b/spec/dummy_app/app/models/skipper.rb
index 852ed07..094558a 100644
--- a/spec/dummy_app/app/models/skipper.rb
+++ b/spec/dummy_app/app/models/skipper.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class Skipper < ActiveRecord::Base
+class Skipper < ApplicationRecord
has_paper_trail ignore: [:created_at], skip: [:another_timestamp]
end
diff --git a/spec/dummy_app/app/models/song.rb b/spec/dummy_app/app/models/song.rb
index 05f823d..3196689 100644
--- a/spec/dummy_app/app/models/song.rb
+++ b/spec/dummy_app/app/models/song.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Song < ActiveRecord::Base
+class Song < ApplicationRecord
has_paper_trail
attribute :name, :string
diff --git a/spec/dummy_app/app/models/thing.rb b/spec/dummy_app/app/models/thing.rb
index 079b1b0..bfb39ec 100644
--- a/spec/dummy_app/app/models/thing.rb
+++ b/spec/dummy_app/app/models/thing.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Thing < ActiveRecord::Base
+class Thing < ApplicationRecord
has_paper_trail versions: {
scope: -> { order("id desc") },
extend: PrefixVersionsInspectWithCount
diff --git a/spec/dummy_app/app/models/tomato.rb b/spec/dummy_app/app/models/tomato.rb
new file mode 100644
index 0000000..42f3e0c
--- /dev/null
+++ b/spec/dummy_app/app/models/tomato.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Tomato < Plant
+ class << self
+ def sti_name
+ "tomato"
+ end
+ end
+end
diff --git a/spec/dummy_app/app/models/translation.rb b/spec/dummy_app/app/models/translation.rb
index cdcc3b3..ec2d581 100644
--- a/spec/dummy_app/app/models/translation.rb
+++ b/spec/dummy_app/app/models/translation.rb
@@ -1,13 +1,9 @@
# frozen_string_literal: true
# Demonstrates the `if` and `unless` configuration options.
-class Translation < ActiveRecord::Base
- # Has a `type` column, but it's not used for STI.
- # TODO: rename column
- self.inheritance_column = nil
-
+class Translation < ApplicationRecord
has_paper_trail(
if: proc { |t| t.language_code == "US" },
- unless: proc { |t| t.type == "DRAFT" }
+ unless: proc { |t| t.draft_status == "DRAFT" }
)
end
diff --git a/spec/dummy_app/app/models/vegetable.rb b/spec/dummy_app/app/models/vegetable.rb
new file mode 100644
index 0000000..cca41cf
--- /dev/null
+++ b/spec/dummy_app/app/models/vegetable.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# See also `Fruit` which uses `JsonVersion`.
+class Vegetable < ApplicationRecord
+ has_paper_trail versions: {
+ class_name: ENV["DB"] == "postgres" ? "JsonbVersion" : "PaperTrail::Version"
+ }, on: %i[create update]
+end
diff --git a/spec/dummy_app/app/models/vehicle.rb b/spec/dummy_app/app/models/vehicle.rb
index 2ed1ba4..8268c24 100644
--- a/spec/dummy_app/app/models/vehicle.rb
+++ b/spec/dummy_app/app/models/vehicle.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Vehicle < ActiveRecord::Base
+class Vehicle < ApplicationRecord
# This STI parent class specifically does not call `has_paper_trail`.
# Of its sub-classes, only `Car` and `Bicycle` are versioned.
belongs_to :owner, class_name: "Person", optional: true
diff --git a/spec/dummy_app/app/models/whatchamajigger.rb b/spec/dummy_app/app/models/whatchamajigger.rb
index 25ccc9d..9f27f1b 100644
--- a/spec/dummy_app/app/models/whatchamajigger.rb
+++ b/spec/dummy_app/app/models/whatchamajigger.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Whatchamajigger < ActiveRecord::Base
+class Whatchamajigger < ApplicationRecord
has_paper_trail
belongs_to :owner, polymorphic: true, optional: true
end
diff --git a/spec/dummy_app/app/models/widget.rb b/spec/dummy_app/app/models/widget.rb
index 84b4021..488b1cf 100644
--- a/spec/dummy_app/app/models/widget.rb
+++ b/spec/dummy_app/app/models/widget.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Widget < ActiveRecord::Base
+class Widget < ApplicationRecord
EXCLUDED_NAME = "Biglet"
has_paper_trail
has_one :wotsit
diff --git a/spec/dummy_app/app/models/wotsit.rb b/spec/dummy_app/app/models/wotsit.rb
index cd4b9ad..3744553 100644
--- a/spec/dummy_app/app/models/wotsit.rb
+++ b/spec/dummy_app/app/models/wotsit.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Wotsit < ActiveRecord::Base
+class Wotsit < ApplicationRecord
has_paper_trail
belongs_to :widget, optional: true
diff --git a/spec/dummy_app/app/versions/abstract_version.rb b/spec/dummy_app/app/versions/abstract_version.rb
index a55e07c..36e40d7 100644
--- a/spec/dummy_app/app/versions/abstract_version.rb
+++ b/spec/dummy_app/app/versions/abstract_version.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AbstractVersion < ActiveRecord::Base
+class AbstractVersion < ApplicationRecord
include PaperTrail::VersionConcern
self.abstract_class = true
end
diff --git a/spec/dummy_app/app/versions/jsonb_version.rb b/spec/dummy_app/app/versions/jsonb_version.rb
new file mode 100644
index 0000000..26e2a03
--- /dev/null
+++ b/spec/dummy_app/app/versions/jsonb_version.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class JsonbVersion < ApplicationRecord
+ include PaperTrail::VersionConcern
+
+ self.table_name = "jsonb_versions"
+end
diff --git a/spec/dummy_app/config/application.rb b/spec/dummy_app/config/application.rb
index 6f4de37..92d3a03 100644
--- a/spec/dummy_app/config/application.rb
+++ b/spec/dummy_app/config/application.rb
@@ -13,7 +13,17 @@ require File.expand_path("boot", __dir__)
module Dummy
class Application < Rails::Application
- config.load_defaults(::Rails.gem_version.segments.take(2).join("."))
+ YAML_COLUMN_PERMITTED_CLASSES = [
+ ::ActiveRecord::Type::Time::Value,
+ ::ActiveSupport::TimeWithZone,
+ ::ActiveSupport::TimeZone,
+ ::BigDecimal,
+ ::Date,
+ ::Symbol,
+ ::Time
+ ].freeze
+
+ config.load_defaults(::ActiveRecord.gem_version.segments.take(2).join("."))
config.encoding = "utf-8"
config.filter_parameters += [:password]
@@ -21,11 +31,14 @@ module Dummy
config.active_support.test_order = :sorted
config.secret_key_base = "A fox regularly kicked the screaming pile of biscuits."
- # In rails >= 6.0, "`.represent_boolean_as_integer=` is now always true,
- # so setting this is deprecated and will be removed in Rails 6.1."
- if ::ENV["DB"] == "sqlite" &&
- ::Gem::Requirement.new("~> 5.2").satisfied_by?(::Rails.gem_version)
- config.active_record.sqlite3.represent_boolean_as_integer = true
+ # `use_yaml_unsafe_load` was added in 5.2.8.1, 6.0.5.1, 6.1.6.1, and 7.0.3.1.
+ # Will be removed in 7.1.0?
+ if ::ActiveRecord.respond_to?(:use_yaml_unsafe_load) # 7.0.3.1
+ ::ActiveRecord.use_yaml_unsafe_load = false
+ ::ActiveRecord.yaml_column_permitted_classes = YAML_COLUMN_PERMITTED_CLASSES
+ elsif ::ActiveRecord::Base.respond_to?(:use_yaml_unsafe_load) # 5.2.8.1, 6.0.5.1, 6.1.6.1
+ ::ActiveRecord::Base.use_yaml_unsafe_load = false
+ ::ActiveRecord::Base.yaml_column_permitted_classes = YAML_COLUMN_PERMITTED_CLASSES
end
end
end
diff --git a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb
index 4a13ac2..a8b8058 100644
--- a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb
+++ b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb
@@ -128,19 +128,23 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
add_index :no_object_versions, %i[item_type item_id]
if ENV["DB"] == "postgres"
- create_table :json_versions, force: true do |t|
- t.string :item_type, null: false
- t.integer :item_id, null: false
- t.string :event, null: false
- t.string :whodunnit
- t.json :object
- t.json :object_changes
- t.datetime :created_at, limit: 6
+ %w[json jsonb].each do |j|
+ table_name = j + "_versions"
+ create_table table_name, force: true do |t|
+ t.string :item_type, null: false
+ t.bigint :item_id, null: false
+ t.string :event, null: false
+ t.string :whodunnit
+ t.public_send j, :object
+ t.public_send j, :object_changes
+ t.datetime :created_at, limit: 6
+ end
+ add_index table_name, %i[item_type item_id]
end
- add_index :json_versions, %i[item_type item_id]
end
create_table :not_on_updates, force: true do |t|
+ t.string :name
t.timestamps null: true, limit: 6
end
@@ -230,6 +234,10 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
t.integer :animal_id
end
+ create_table :plants, force: true do |t|
+ t.string :species # custom single table inheritance column
+ end
+
create_table :documents, force: true do |t|
t.string :name
end
@@ -245,10 +253,10 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
end
create_table :translations, force: true do |t|
- t.string :headline
t.string :content
+ t.string :draft_status
+ t.string :headline
t.string :language_code
- t.string :type
end
create_table :gadgets, force: true do |t|
@@ -260,6 +268,7 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
create_table :customers, force: true do |t|
t.string :name
+ t.datetime :touched_at, limit: 6
end
create_table :orders, force: true do |t|
@@ -273,8 +282,9 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
end
create_table :fruits, force: true do |t|
- t.string :name
t.string :color
+ t.integer :mass
+ t.string :name
end
create_table :boolits, force: true do |t|
@@ -354,6 +364,12 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
t.integer :parent_id
t.integer :partner_id
end
+
+ create_table :vegetables, force: true do |t|
+ t.string :color
+ t.integer :mass
+ t.string :name
+ end
end
def down
diff --git a/spec/generators/paper_trail/install_generator_spec.rb b/spec/generators/paper_trail/install_generator_spec.rb
index 10365b2..34f1386 100644
--- a/spec/generators/paper_trail/install_generator_spec.rb
+++ b/spec/generators/paper_trail/install_generator_spec.rb
@@ -33,9 +33,9 @@ RSpec.describe PaperTrail::InstallGenerator, type: :generator do
}.call
expected_item_type_options = lambda {
if described_class::MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name)
- ", { null: false, limit: 191 }"
+ ", null: false, limit: 191"
else
- ", { null: false }"
+ ", null: false"
end
}.call
expect(destination_root).to(
@@ -102,4 +102,26 @@ RSpec.describe PaperTrail::InstallGenerator, type: :generator do
)
end
end
+
+ describe "`--uuid` option set to `true`" do
+ before do
+ prepare_destination
+ run_generator %w[--uuid]
+ end
+
+ it "generates a migration for creating the 'versions' table with item_id type uuid" do
+ expected_item_id_type = "string"
+ expect(destination_root).to(
+ have_structure {
+ directory("db") {
+ directory("migrate") {
+ migration("create_versions") {
+ contains "t.#{expected_item_id_type} :item_id, null: false"
+ }
+ }
+ }
+ }
+ )
+ end
+ end
end
diff --git a/spec/models/animal_spec.rb b/spec/models/animal_spec.rb
index e1e1ed6..20b7485 100644
--- a/spec/models/animal_spec.rb
+++ b/spec/models/animal_spec.rb
@@ -4,8 +4,8 @@ require "spec_helper"
RSpec.describe Animal, type: :model, versioning: true do
it "baseline test setup" do
- expect(Animal.new).to be_versioned
- expect(Animal.inheritance_column).to eq("species")
+ expect(described_class.new).to be_versioned
+ expect(described_class.inheritance_column).to eq("species")
end
describe "#descends_from_active_record?" do
@@ -15,7 +15,7 @@ RSpec.describe Animal, type: :model, versioning: true do
end
it "works with custom STI inheritance column" do
- animal = Animal.create(name: "Animal")
+ animal = described_class.create(name: "Animal")
animal.update(name: "Animal from the Muppets")
animal.update(name: "Animal Muppet")
animal.destroy
@@ -46,7 +46,7 @@ RSpec.describe Animal, type: :model, versioning: true do
it "allows the inheritance_column (species) to be updated" do
cat = Cat.create!(name: "Leo")
cat.update(name: "Spike", species: "Dog")
- dog = Animal.find(cat.id)
+ dog = described_class.find(cat.id)
expect(dog).to be_instance_of(Dog)
end
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
index 8526332..d8ec2b1 100644
--- a/spec/models/article_spec.rb
+++ b/spec/models/article_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates an ignored column" do
+ context "when updating an ignored column" do
it "not change the number of versions" do
article = described_class.create
article.update(title: "My first title")
@@ -19,7 +19,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates an ignored column with truly Proc" do
+ context "when updating an ignored column with truly Proc" do
it "not change the number of versions" do
article = described_class.create
article.update(abstract: "ignore abstract")
@@ -27,7 +27,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates an ignored column with falsy Proc" do
+ context "when updating an ignored column with falsy Proc" do
it "change the number of versions" do
article = described_class.create
article.update(abstract: "do not ignore abstract!")
@@ -35,7 +35,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates an ignored column, ignored with truly Proc and a selected column" do
+ context "when updating an ignored column, ignored with truly Proc and a selected column" do
it "change the number of versions" do
article = described_class.create
article.update(
@@ -59,7 +59,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates an ignored column, ignored with falsy Proc and a selected column" do
+ context "when updating an ignored column, ignored with falsy Proc and a selected column" do
it "change the number of versions" do
article = described_class.create
article.update(
@@ -86,7 +86,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates a selected column" do
+ context "when updating a selected column" do
it "change the number of versions" do
article = described_class.create
article.update(content: "Some text here.")
@@ -95,7 +95,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates a non-ignored and non-selected column" do
+ context "when updating a non-ignored and non-selected column" do
it "not change the number of versions" do
article = described_class.create
article.update(abstract: "Other abstract")
@@ -103,7 +103,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates a skipped column" do
+ context "when updating a skipped column" do
it "not change the number of versions" do
article = described_class.create
article.update(file_upload: "Your data goes here")
@@ -111,7 +111,7 @@ RSpec.describe Article, type: :model, versioning: true do
end
end
- context "which updates a skipped column and a selected column" do
+ context "when updating a skipped column and a selected column" do
it "change the number of versions" do
article = described_class.create
article.update(
@@ -141,7 +141,7 @@ RSpec.describe Article, type: :model, versioning: true do
).to(eq("content" => [nil, "Some text here."]))
end
- context "and when updated again" do
+ context "when updated again" do
it "have removed the skipped attributes when saving the previous version" do
article = described_class.create
article.update(
@@ -185,4 +185,79 @@ RSpec.describe Article, type: :model, versioning: true do
expect(article.versions.map(&:event)).to(match_array(%w[create destroy]))
end
end
+
+ context "with an item" do
+ let(:article) { described_class.new(title: initial_title) }
+ let(:initial_title) { "Foobar" }
+
+ context "when it is created" do
+ before { article.save }
+
+ it "store fixed meta data" do
+ expect(article.versions.last.answer).to(eq(42))
+ end
+
+ it "store dynamic meta data which is independent of the item" do
+ expect(article.versions.last.question).to(eq("31 + 11 = 42"))
+ end
+
+ it "store dynamic meta data which depends on the item" do
+ expect(article.versions.last.article_id).to(eq(article.id))
+ end
+
+ it "store dynamic meta data based on a method of the item" do
+ expect(article.versions.last.action).to(eq(article.action_data_provider_method))
+ end
+
+ it "store dynamic meta data based on an attribute of the item at creation" do
+ expect(article.versions.last.title).to(eq(initial_title))
+ end
+ end
+
+ context "when it is created, then updated" do
+ before do
+ article.save
+ article.update!(content: "Better text.", title: "Rhubarb")
+ end
+
+ it "store fixed meta data" do
+ expect(article.versions.last.answer).to(eq(42))
+ end
+
+ it "store dynamic meta data which is independent of the item" do
+ expect(article.versions.last.question).to(eq("31 + 11 = 42"))
+ end
+
+ it "store dynamic meta data which depends on the item" do
+ expect(article.versions.last.article_id).to(eq(article.id))
+ end
+
+ it "store dynamic meta data based on an attribute of the item prior to the update" do
+ expect(article.versions.last.title).to(eq(initial_title))
+ end
+ end
+
+ context "when it is created, then destroyed" do
+ before do
+ article.save
+ article.destroy
+ end
+
+ it "store fixed metadata" do
+ expect(article.versions.last.answer).to(eq(42))
+ end
+
+ it "store dynamic metadata which is independent of the item" do
+ expect(article.versions.last.question).to(eq("31 + 11 = 42"))
+ end
+
+ it "store dynamic metadata which depends on the item" do
+ expect(article.versions.last.article_id).to(eq(article.id))
+ end
+
+ it "store dynamic metadata based on attribute of item prior to destruction" do
+ expect(article.versions.last.title).to(eq(initial_title))
+ end
+ end
+ end
end
diff --git a/spec/models/book_spec.rb b/spec/models/book_spec.rb
new file mode 100644
index 0000000..d51f15f
--- /dev/null
+++ b/spec/models/book_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Book, versioning: true do
+ context "with :has_many :through" do
+ it "store version on source <<" do
+ book = described_class.create(title: "War and Peace")
+ dostoyevsky = Person.create(name: "Dostoyevsky")
+ Person.create(name: "Solzhenitsyn")
+ count = PaperTrail::Version.count
+ (book.authors << dostoyevsky)
+ expect((PaperTrail::Version.count - count)).to(eq(1))
+ expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
+ end
+
+ it "store version on source create" do
+ book = described_class.create(title: "War and Peace")
+ Person.create(name: "Dostoyevsky")
+ Person.create(name: "Solzhenitsyn")
+ count = PaperTrail::Version.count
+ book.authors.create(name: "Tolstoy")
+ expect((PaperTrail::Version.count - count)).to(eq(2))
+ expect(
+ [PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
+ ).to match_array([Person.last, Authorship.last])
+ end
+
+ it "store version on join destroy" do
+ book = described_class.create(title: "War and Peace")
+ dostoyevsky = Person.create(name: "Dostoyevsky")
+ Person.create(name: "Solzhenitsyn")
+ (book.authors << dostoyevsky)
+ count = PaperTrail::Version.count
+ book.authorships.reload.last.destroy
+ expect((PaperTrail::Version.count - count)).to(eq(1))
+ expect(PaperTrail::Version.last.reify.book).to(eq(book))
+ expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
+ end
+
+ it "store version on join clear" do
+ book = described_class.create(title: "War and Peace")
+ dostoyevsky = Person.create(name: "Dostoyevsky")
+ Person.create(name: "Solzhenitsyn")
+ book.authors << dostoyevsky
+ count = PaperTrail::Version.count
+ book.authorships.reload.destroy_all
+ expect((PaperTrail::Version.count - count)).to(eq(1))
+ expect(PaperTrail::Version.last.reify.book).to(eq(book))
+ expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
+ end
+ end
+
+ context "when a persisted record is updated then destroyed" do
+ it "has changes" do
+ book = described_class.create! title: "A"
+ changes = YAML.load book.versions.last.attributes["object_changes"]
+ expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])
+
+ book.update! title: "B"
+ changes = YAML.load book.versions.last.attributes["object_changes"]
+ expect(changes).to eq("title" => %w[A B])
+
+ book.destroy
+ changes = YAML.load book.versions.last.attributes["object_changes"]
+ expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
+ end
+ end
+end
diff --git a/spec/models/boolit_spec.rb b/spec/models/boolit_spec.rb
index 58a4547..c022bce 100644
--- a/spec/models/boolit_spec.rb
+++ b/spec/models/boolit_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
require "support/custom_json_serializer"
RSpec.describe Boolit, type: :model, versioning: true do
- let(:boolit) { Boolit.create! }
+ let(:boolit) { described_class.create! }
before { boolit.update!(name: FFaker::Name.name) }
@@ -16,11 +16,11 @@ RSpec.describe Boolit, type: :model, versioning: true do
expect { boolit.versions.last.reify.save! }.not_to raise_error
end
- context "Instance falls out of default scope" do
+ context "when Instance falls out of default scope" do
before { boolit.update!(scoped: false) }
it "is NOT scoped" do
- expect(Boolit.first).to be_nil
+ expect(described_class.first).to be_nil
end
it "still can be reified and persisted" do
diff --git a/spec/models/car_spec.rb b/spec/models/car_spec.rb
index cf0e1d9..37f98c8 100644
--- a/spec/models/car_spec.rb
+++ b/spec/models/car_spec.rb
@@ -7,9 +7,28 @@ RSpec.describe Car, type: :model do
describe "changeset", versioning: true do
it "has the expected keys (see issue 738)" do
- car = Car.create!(name: "Alice")
+ car = described_class.create!(name: "Alice")
car.update(name: "Bob")
assert_includes car.versions.last.changeset.keys, "name"
end
end
+
+ describe "attributes and accessors", versioning: true do
+ it "reifies attributes that are not AR attributes" do
+ car = described_class.create name: "Pinto", color: "green"
+ car.update color: "yellow"
+ car.update color: "brown"
+ expect(car.versions.second.reify.color).to eq("yellow")
+ end
+
+ it "reifies attributes that once were attributes but now just attr_accessor" do
+ car = described_class.create name: "Pinto", color: "green"
+ car.update color: "yellow"
+ changes = PaperTrail::Serializers::YAML.load(car.versions.last.attributes["object"])
+ changes[:top_speed] = 80
+ car.versions.first.update object: changes.to_yaml
+ car.reload
+ expect(car.versions.first.reify.top_speed).to eq(80)
+ end
+ end
end
diff --git a/spec/models/custom_primary_key_record_spec.rb b/spec/models/custom_primary_key_record_spec.rb
index 519f0ff..1c1a125 100644
--- a/spec/models/custom_primary_key_record_spec.rb
+++ b/spec/models/custom_primary_key_record_spec.rb
@@ -12,9 +12,9 @@ RSpec.describe CustomPrimaryKeyRecord, type: :model do
version = custom_primary_key_record.versions.last
expect(version).to be_a(CustomPrimaryKeyRecordVersion)
version_from_db = CustomPrimaryKeyRecordVersion.last
- expect(version_from_db.reify).to be_a(CustomPrimaryKeyRecord)
+ expect(version_from_db.reify).to be_a(described_class)
custom_primary_key_record.destroy
- expect(CustomPrimaryKeyRecordVersion.last.reify).to be_a(CustomPrimaryKeyRecord)
+ expect(CustomPrimaryKeyRecordVersion.last.reify).to be_a(described_class)
end
end
end
diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb
index 59e0a53..19bbe80 100644
--- a/spec/models/document_spec.rb
+++ b/spec/models/document_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe Document, type: :model, versioning: true do
describe "have_a_version_with matcher" do
it "works with custom versions association" do
- document = Document.create!(name: "Foo")
+ document = described_class.create!(name: "Foo")
document.update!(name: "Bar")
expect(document).to have_a_version_with(name: "Foo")
end
@@ -13,7 +13,7 @@ RSpec.describe Document, type: :model, versioning: true do
describe "#paper_trail.next_version" do
it "returns the expected document" do
- doc = Document.create
+ doc = described_class.create
doc.update(name: "Doc 1")
reified = doc.paper_trail_versions.last.reify
expect(doc.name).to(eq(reified.paper_trail.next_version.name))
@@ -22,7 +22,7 @@ RSpec.describe Document, type: :model, versioning: true do
describe "#paper_trail.previous_version" do
it "returns the expected document" do
- doc = Document.create
+ doc = described_class.create
doc.update(name: "Doc 1")
doc.update(name: "Doc 2")
expect(doc.paper_trail_versions.length).to(eq(3))
@@ -32,7 +32,7 @@ RSpec.describe Document, type: :model, versioning: true do
describe "#paper_trail_versions" do
it "returns the expected version records" do
- doc = Document.create
+ doc = described_class.create
doc.update(name: "Doc 1")
expect(doc.paper_trail_versions.length).to(eq(2))
expect(doc.paper_trail_versions.map(&:event)).to(
@@ -43,7 +43,7 @@ RSpec.describe Document, type: :model, versioning: true do
describe "#versions" do
it "does not respond to versions method" do
- doc = Document.create
+ doc = described_class.create
doc.update(name: "Doc 1")
expect(doc).not_to respond_to(:versions)
end
diff --git a/spec/models/family/celebrity_family_spec.rb b/spec/models/family/celebrity_family_spec.rb
index b55e70b..f7c70aa 100644
--- a/spec/models/family/celebrity_family_spec.rb
+++ b/spec/models/family/celebrity_family_spec.rb
@@ -29,7 +29,7 @@ module Family
end
describe "#reify" do
- context "belongs_to" do
+ context "with belongs_to" do
it "uses the correct item_subtype" do
parent = described_class.new(name: "Jermaine Jackson")
parent.path_to_stardom = "Emulating Motown greats such as the Temptations and "\
@@ -51,7 +51,7 @@ module Family
end
end
- context "has_many" do
+ context "with has_many" do
it "uses the correct item_type in queries" do
parent = described_class.new(name: "Gomez Addams")
parent.path_to_stardom = "Buy a Victorian house next to a sprawling graveyard, "\
@@ -70,7 +70,7 @@ module Family
end
end
- context "has_many through" do
+ context "with has_many through" do
it "uses the correct item_type in queries" do
parent = described_class.new(name: "Grandad")
parent.path_to_stardom = "Took a suitcase and started running a market trading "\
@@ -90,7 +90,7 @@ module Family
end
end
- context "has_one" do
+ context "with has_one" do
it "uses the correct item_type in queries" do
parent = described_class.new(name: "Minnie Marx")
parent.path_to_stardom = "Gain a relentless dedication to the stage by having a "\
diff --git a/spec/models/foo_widget_spec.rb b/spec/models/foo_widget_spec.rb
new file mode 100644
index 0000000..8e3bbfc
--- /dev/null
+++ b/spec/models/foo_widget_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+require "support/performance_helpers"
+
+RSpec.describe(FooWidget, versioning: true) do
+ context "with a subclass" do
+ let(:foo) { described_class.create }
+
+ before do
+ foo.update!(name: "Foo")
+ end
+
+ it "reify with the correct type" do
+ expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
+ expect(PaperTrail::Version.last.next).to(be_nil)
+ end
+
+ it "returns the correct originator" do
+ PaperTrail.request.whodunnit = "Ben"
+ foo.update_attribute(:name, "Geoffrey")
+ expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
+ end
+
+ context "when destroyed" do
+ before { foo.destroy }
+
+ it "reify with the correct type" do
+ assert_kind_of(described_class, foo.versions.last.reify)
+ expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
+ expect(PaperTrail::Version.last.next).to(be_nil)
+ end
+ end
+ end
+end
diff --git a/spec/models/fruit_spec.rb b/spec/models/fruit_spec.rb
index acb70fc..10e12bc 100644
--- a/spec/models/fruit_spec.rb
+++ b/spec/models/fruit_spec.rb
@@ -2,19 +2,54 @@
require "spec_helper"
-if ENV["DB"] == "postgres" || JsonVersion.table_exists?
+if ENV["DB"] == "postgres" && JsonVersion.table_exists?
RSpec.describe Fruit, type: :model, versioning: true do
describe "have_a_version_with_changes matcher" do
it "works with Fruit because Fruit uses JsonVersion" do
# As of PT 9.0.0, with_version_changes only supports json(b) columns,
# so that's why were testing the have_a_version_with_changes matcher
# here.
- banana = Fruit.create!(color: "Red", name: "Banana")
+ banana = described_class.create!(color: "Red", name: "Banana")
banana.update!(color: "Yellow")
expect(banana).to have_a_version_with_changes(color: "Yellow")
expect(banana).not_to have_a_version_with_changes(color: "Pink")
expect(banana).not_to have_a_version_with_changes(color: "Yellow", name: "Kiwi")
end
end
+
+ describe "queries of versions", versioning: true do
+ let!(:fruit) { described_class.create(name: "Apple", mass: 1, color: "green") }
+
+ before do
+ described_class.create(name: "Pear")
+ fruit.update(name: "Fidget")
+ fruit.update(name: "Digit")
+ end
+
+ it "return the fruit whose name has changed" do
+ result = JsonVersion.where_attribute_changes(:name).map(&:item)
+ expect(result).to include(fruit)
+ end
+
+ it "returns the fruit whose name was Fidget" do
+ result = JsonVersion.where_object_changes_from({ name: "Fidget" }).map(&:item)
+ expect(result).to include(fruit)
+ end
+
+ it "returns the fruit whose name became Digit" do
+ result = JsonVersion.where_object_changes_to({ name: "Digit" }).map(&:item)
+ expect(result).to include(fruit)
+ end
+
+ it "returns the fruit where the object was named Fidget before it changed" do
+ result = JsonVersion.where_object({ name: "Fidget" }).map(&:item)
+ expect(result).to include(fruit)
+ end
+
+ it "returns the fruit that changed to Fidget" do
+ result = JsonVersion.where_object_changes({ name: "Fidget" }).map(&:item)
+ expect(result).to include(fruit)
+ end
+ end
end
end
diff --git a/spec/models/gadget_spec.rb b/spec/models/gadget_spec.rb
index 531f116..5c8bd06 100644
--- a/spec/models/gadget_spec.rb
+++ b/spec/models/gadget_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
RSpec.describe Gadget, type: :model do
- let(:gadget) { Gadget.create!(name: "Wrench", brand: "Acme") }
+ let(:gadget) { described_class.create!(name: "Wrench", brand: "Acme") }
it { is_expected.to be_versioned }
@@ -12,13 +12,13 @@ RSpec.describe Gadget, type: :model do
expect { gadget.update_attribute(:name, "Hammer") }.to(change { gadget.versions.size }.by(1))
end
- context "ignored via symbol" do
+ context "when ignored via symbol" do
it "doesn't generate a version" do
expect { gadget.update_attribute(:brand, "Picard") }.not_to(change { gadget.versions.size })
end
end
- context "ignored via Hash" do
+ context "when ignored via Hash" do
it "generates a version when the ignored attribute isn't true" do
expect { gadget.update_attribute(:color, "Blue") }.to(change { gadget.versions.size }.by(1))
expect(gadget.versions.last.changeset.keys).to eq %w[color updated_at]
@@ -35,7 +35,11 @@ RSpec.describe Gadget, type: :model do
gadget.update_attribute(:updated_at, Time.current + 1)
}.to(change { gadget.versions.size }.by(1))
expect(
- YAML.load(gadget.versions.last.object_changes).keys
+ if ::YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load(gadget.versions.last.object_changes).keys
+ else
+ YAML.load(gadget.versions.last.object_changes).keys
+ end
).to eq(["updated_at"])
end
end
diff --git a/spec/models/joined_version_spec.rb b/spec/models/joined_version_spec.rb
index d144cfe..8046f1d 100644
--- a/spec/models/joined_version_spec.rb
+++ b/spec/models/joined_version_spec.rb
@@ -4,10 +4,10 @@ require "spec_helper"
RSpec.describe JoinedVersion, type: :model, versioning: true do
let(:widget) { Widget.create!(name: FFaker::Name.name) }
- let(:version) { JoinedVersion.first }
+ let(:version) { described_class.first }
describe "default_scope" do
- it { expect(JoinedVersion.default_scopes).not_to be_empty }
+ it { expect(described_class.default_scopes).not_to be_empty }
end
describe "VersionConcern::ClassMethods" do
@@ -15,19 +15,19 @@ RSpec.describe JoinedVersion, type: :model, versioning: true do
describe "#subsequent" do
it "does not raise error when there is a default_scope that joins" do
- JoinedVersion.subsequent(version).first
+ described_class.subsequent(version).first
end
end
describe "#preceding" do
it "does not raise error when there is a default scope that joins" do
- JoinedVersion.preceding(version).first
+ described_class.preceding(version).first
end
end
describe "#between" do
it "does not raise error when there is a default scope that joins" do
- JoinedVersion.between(Time.current, 1.minute.from_now).first
+ described_class.between(Time.current, 1.minute.from_now).first
end
end
end
diff --git a/spec/models/json_version_spec.rb b/spec/models/json_version_spec.rb
index d1d7253..72400e3 100644
--- a/spec/models/json_version_spec.rb
+++ b/spec/models/json_version_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
# The `json_versions` table tests postgres' `json` data type. So, that
# table is only created when testing against postgres.
if JsonVersion.table_exists?
- RSpec.describe JsonVersion, type: :model do
+ RSpec.describe JsonVersion, type: :model, versioning: true do
it "includes the VersionConcern module" do
expect(described_class).to include(PaperTrail::VersionConcern)
end
@@ -23,14 +23,14 @@ if JsonVersion.table_exists?
).to eq(0)
end
- context "invalid arguments" do
+ context "with invalid arguments" do
it "raises an error" do
expect { described_class.where_object(:foo) }.to raise_error(ArgumentError)
expect { described_class.where_object([]) }.to raise_error(ArgumentError)
end
end
- context "valid arguments", versioning: true do
+ context "with valid arguments", versioning: true do
it "locates versions according to their `object` contents" do
fruit = Fruit.create!(name: "apple")
expect(fruit.versions.length).to eq(1)
@@ -65,14 +65,14 @@ if JsonVersion.table_exists?
).to eq(0)
end
- context "invalid arguments" do
+ context "with invalid arguments" do
it "raises an error" do
expect { described_class.where_object_changes(:foo) }.to raise_error(ArgumentError)
expect { described_class.where_object_changes([]) }.to raise_error(ArgumentError)
end
end
- context "valid arguments", versioning: true do
+ context "with valid arguments", versioning: true do
it "finds versions according to their `object_changes` contents" do
fruit = Fruit.create!(name: "apple")
fruit.update!(name: "banana", color: "red")
diff --git a/spec/models/no_object_spec.rb b/spec/models/no_object_spec.rb
index 4c4e9e7..95f6249 100644
--- a/spec/models/no_object_spec.rb
+++ b/spec/models/no_object_spec.rb
@@ -27,7 +27,11 @@ RSpec.describe NoObject, versioning: true do
# New feature: destroy populates object_changes
# https://github.com/paper-trail-gem/paper_trail/pull/1123
- h = YAML.load a["object_changes"]
+ h = if ::YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load a["object_changes"]
+ else
+ YAML.load a["object_changes"]
+ end
expect(h["id"]).to eq([n.id, nil])
expect(h["letter"]).to eq([n.letter, nil])
expect(h["created_at"][0]).to be_present
@@ -38,12 +42,12 @@ RSpec.describe NoObject, versioning: true do
describe "reify" do
it "raises error" do
- n = NoObject.create!(letter: "A")
+ n = described_class.create!(letter: "A")
v = n.versions.last
expect { v.reify }.to(
raise_error(
- ::RuntimeError,
- "reify can't be called without an object column"
+ ::PaperTrail::Error,
+ "reify requires an object column"
)
)
end
@@ -51,13 +55,13 @@ RSpec.describe NoObject, versioning: true do
describe "where_object" do
it "raises error" do
- n = NoObject.create!(letter: "A")
+ n = described_class.create!(letter: "A")
expect {
n.versions.where_object(foo: "bar")
}.to(
raise_error(
- ::RuntimeError,
- "where_object can't be called without an object column"
+ ::PaperTrail::Error,
+ "where_object requires an object column"
)
)
end
diff --git a/spec/models/not_on_update_spec.rb b/spec/models/not_on_update_spec.rb
index 2ab15d7..c811c56 100644
--- a/spec/models/not_on_update_spec.rb
+++ b/spec/models/not_on_update_spec.rb
@@ -11,5 +11,12 @@ RSpec.describe NotOnUpdate, type: :model do
PaperTrail::Version.count
}.by(+1)
end
+
+ it "captures changes when in_after_callback is true" do
+ record.name = "test"
+ record.paper_trail.save_with_version(in_after_callback: true)
+ changeset = record.versions.last.changeset
+ expect(changeset[:name]).to eq([nil, "test"])
+ end
end
end
diff --git a/spec/models/order_spec.rb b/spec/models/order_spec.rb
new file mode 100644
index 0000000..99665ac
--- /dev/null
+++ b/spec/models/order_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Order, type: :model, versioning: true do
+ context "when the record destroyed" do
+ it "creates a version record for association" do
+ customer = Customer.create!
+ described_class.create!(customer: customer)
+ described_class.destroy_all
+
+ expect(customer.versions.count).to(eq(3))
+ end
+ end
+end
diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb
index df47fa1..bd44f19 100644
--- a/spec/models/person_spec.rb
+++ b/spec/models/person_spec.rb
@@ -9,21 +9,21 @@ require "spec_helper"
RSpec.describe Person, type: :model, versioning: true do
describe "#time_zone" do
it "returns an ActiveSupport::TimeZone" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
expect(person.time_zone.class).to(eq(ActiveSupport::TimeZone))
end
end
context "when the model is saved" do
it "version.object_changes should store long serialization of TimeZone object" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
len = person.versions.last.object_changes.length
expect((len < 105)).to(be_truthy)
end
it "version.object_changes attribute should have stored the value from serializer" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
as_stored_in_version = HashWithIndifferentAccess[
YAML.load(person.versions.last.object_changes)
@@ -34,14 +34,14 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.changeset should convert attribute to original, unserialized value" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
unserialized_value = Person::TimeZoneSerializer.load(person.time_zone)
expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value))
end
it "record.changes (before save) returns the original, unserialized values" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
changes_before_save = person.changes.dup
person.save!
expect(
@@ -50,7 +50,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.changeset should be the same as record.changes was before the save" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
changes_before_save = person.changes.dup
person.save!
actual = person.versions.last.changeset.delete_if { |k, _v| (k.to_sym == :id) }
@@ -61,7 +61,7 @@ RSpec.describe Person, type: :model, versioning: true do
context "when that attribute is updated" do
it "object should not store long serialization of TimeZone object" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save!
@@ -70,7 +70,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "object_changes should not store long serialization of TimeZone object" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save!
@@ -79,7 +79,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.object attribute should have stored value from serializer" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
attribute_value_before_change = person.time_zone
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
@@ -93,7 +93,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.object_changes attribute should have stored value from serializer" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save!
@@ -106,7 +106,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.reify should convert attribute to original, unserialized value" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
attribute_value_before_change = person.time_zone
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
@@ -116,7 +116,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.changeset should convert attribute to original, unserialized value" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save!
@@ -125,7 +125,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "record.changes (before save) returns the original, unserialized values" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
changes_before_save = person.changes.dup
@@ -136,7 +136,7 @@ RSpec.describe Person, type: :model, versioning: true do
end
it "version.changeset should be the same as record.changes was before the save" do
- person = Person.new(time_zone: "Samoa")
+ person = described_class.new(time_zone: "Samoa")
person.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
changes_before_save = person.changes.dup
@@ -151,7 +151,7 @@ RSpec.describe Person, type: :model, versioning: true do
describe "#cars and bicycles" do
it "can be reified" do
- person = Person.create(name: "Frank")
+ person = described_class.create(name: "Frank")
car = Car.create(name: "BMW 325")
bicycle = Bicycle.create(name: "BMX 1.0")
diff --git a/spec/models/pet_spec.rb b/spec/models/pet_spec.rb
index 6a991a0..061e86b 100644
--- a/spec/models/pet_spec.rb
+++ b/spec/models/pet_spec.rb
@@ -5,7 +5,7 @@ require "rails/generators"
RSpec.describe Pet, type: :model, versioning: true do
it "baseline test setup" do
- expect(Pet.new).to be_versioned
+ expect(described_class.new).to be_versioned
end
it "can be reified" do
@@ -13,8 +13,8 @@ RSpec.describe Pet, type: :model, versioning: true do
dog = Dog.create(name: "Snoopy")
cat = Cat.create(name: "Garfield")
- person.pets << Pet.create(animal: dog)
- person.pets << Pet.create(animal: cat)
+ person.pets << described_class.create(animal: dog)
+ person.pets << described_class.create(animal: cat)
person.update(name: "Steve")
dog.update(name: "Beethoven")
@@ -24,7 +24,7 @@ RSpec.describe Pet, type: :model, versioning: true do
expect(person.reload.versions.length).to(eq(3))
end
- context "Older version entry present where item_type refers to the base_class" do
+ context "when an older version entry's item_type refers to the base_class" do
let(:cat) { Cat.create(name: "Garfield") } # Index 0
let(:animal) { Animal.create } # Index 4
@@ -83,7 +83,7 @@ RSpec.describe Pet, type: :model, versioning: true do
# After creating a bunch of records above, we change the inheritance_column
# so that we can demonstrate passing hints to the migration generator.
- context "simulate a historical change to inheritance_column" do
+ context "when there was a historical change to inheritance_column" do
before do
Animal.inheritance_column = "species_xyz"
end
diff --git a/spec/models/plant_spec.rb b/spec/models/plant_spec.rb
new file mode 100644
index 0000000..4656b1f
--- /dev/null
+++ b/spec/models/plant_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Plant, type: :model, versioning: true do
+ it "baseline test setup" do
+ expect(described_class.new).to be_versioned
+ expect(described_class.inheritance_column).to eq("species")
+ end
+
+ describe "#descends_from_active_record?" do
+ it "returns true, meaning that Animal is not an STI subclass" do
+ expect(described_class.descends_from_active_record?).to eq(true)
+ end
+ end
+
+ it "works with non standard STI column contents" do
+ plant = described_class.create
+ plant.destroy
+
+ tomato = Tomato.create
+ tomato.destroy
+
+ reified = plant.versions.last.reify
+ expect(reified.class).to eq(described_class)
+
+ reified = tomato.versions.last.reify
+ expect(reified.class).to eq(Tomato)
+ end
+end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index 08bb5e2..59bf49f 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -5,29 +5,29 @@ require "spec_helper"
# The `Post` model uses a custom version class, `PostVersion`
RSpec.describe Post, type: :model, versioning: true do
it "inserts records into the correct table, post_versions" do
- post = Post.create
+ post = described_class.create
expect(PostVersion.count).to(eq(1))
post.update(content: "Some new content")
expect(PostVersion.count).to(eq(2))
expect(PaperTrail::Version.count).to(eq(0))
end
- context "on the first version" do
+ context "with the first version" do
it "have the correct index" do
- post = Post.create
+ post = described_class.create
version = post.versions.first
expect(version.index).to(eq(0))
end
end
it "have versions of the custom class" do
- post = Post.create
+ post = described_class.create
expect(post.versions.first.class.name).to(eq("PostVersion"))
end
describe "#changeset" do
it "returns nil because the object_changes column doesn't exist" do
- post = Post.create
+ post = described_class.create
post.update(content: "Some new content")
expect(post.versions.last.changeset).to(be_nil)
end
diff --git a/spec/models/post_with_status_spec.rb b/spec/models/post_with_status_spec.rb
index 3916b73..cd11f05 100644
--- a/spec/models/post_with_status_spec.rb
+++ b/spec/models/post_with_status_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe PostWithStatus, type: :model do
assert_equal %w[draft published], version.changeset["status"]
end
- context "storing enum object_changes" do
+ context "when storing enum object_changes" do
it "saves the enum value properly in versions object_changes" do
post.published!
post.archived!
diff --git a/spec/models/skipper_spec.rb b/spec/models/skipper_spec.rb
index 33242fa..2e4189e 100644
--- a/spec/models/skipper_spec.rb
+++ b/spec/models/skipper_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe Skipper, type: :model, versioning: true do
it { is_expected.to be_versioned }
describe "#update!", versioning: true do
- context "updating a skipped attribute" do
+ context "when updating a skipped attribute" do
let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
it "does not create a version" do
- skipper = Skipper.create!(another_timestamp: t1)
+ skipper = described_class.create!(another_timestamp: t1)
expect {
skipper.update!(another_timestamp: t2)
}.not_to(change { skipper.versions.length })
@@ -19,13 +19,55 @@ RSpec.describe Skipper, type: :model, versioning: true do
end
end
+ describe "#touch" do
+ let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
+ let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
+
+ if ActiveRecord.gem_version >= Gem::Version.new("6")
+ it "does not create a version for skipped attributes" do
+ skipper = described_class.create!(another_timestamp: t1)
+ expect {
+ skipper.touch(:another_timestamp, time: t2)
+ }.not_to(change { skipper.versions.length })
+ end
+
+ it "does not create a version for ignored attributes" do
+ skipper = described_class.create!(created_at: t1)
+ expect {
+ skipper.touch(:created_at, time: t2)
+ }.not_to(change { skipper.versions.length })
+ end
+ else
+ it "creates a version even for skipped attributes" do
+ skipper = described_class.create!(another_timestamp: t1)
+ expect {
+ skipper.touch(:another_timestamp, time: t2)
+ }.to(change { skipper.versions.length })
+ end
+
+ it "creates a version even for ignored attributes" do
+ skipper = described_class.create!(created_at: t1)
+ expect {
+ skipper.touch(:created_at, time: t2)
+ }.to(change { skipper.versions.length })
+ end
+ end
+
+ it "creates a version for non-skipped timestamps" do
+ skipper = described_class.create!
+ expect {
+ skipper.touch
+ }.to(change { skipper.versions.length })
+ end
+ end
+
describe "#reify" do
let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
context "without preserve (default)" do
it "has no timestamp" do
- skipper = Skipper.create!(another_timestamp: t1)
+ skipper = described_class.create!(another_timestamp: t1)
skipper.update!(another_timestamp: t2, name: "Foobar")
skipper = skipper.versions.last.reify
expect(skipper.another_timestamp).to be(nil)
@@ -34,7 +76,7 @@ RSpec.describe Skipper, type: :model, versioning: true do
context "with preserve" do
it "preserves its timestamp" do
- skipper = Skipper.create!(another_timestamp: t1)
+ skipper = described_class.create!(another_timestamp: t1)
skipper.update!(another_timestamp: t2, name: "Foobar")
skipper = skipper.versions.last.reify(unversioned_attributes: :preserve)
expect(skipper.another_timestamp).to eq(t2)
diff --git a/spec/models/song_spec.rb b/spec/models/song_spec.rb
index 91f44e9..370db79 100644
--- a/spec/models/song_spec.rb
+++ b/spec/models/song_spec.rb
@@ -14,4 +14,25 @@ require "spec_helper"
expect(result.event).to eq("create")
end
end
+
+ context "when the default accessor, length=, is overwritten" do
+ it "returns overwritten value on reified instance" do
+ song = Song.create(length: 4)
+ song.update(length: 5)
+ expect(song.length).to(eq(5))
+ expect(song.versions.last.reify.length).to(eq(4))
+ end
+ end
+
+ context "when song name is a virtual attribute (no such db column)" do
+ it "returns overwritten virtual attribute on the reified instance" do
+ song = Song.create(length: 4)
+ song.update(length: 5)
+ song.name = "Good Vibrations"
+ song.save
+ song.name = "Yellow Submarine"
+ expect(song.name).to(eq("Yellow Submarine"))
+ expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
+ end
+ end
end
diff --git a/spec/models/thing_spec.rb b/spec/models/thing_spec.rb
index ac7b852..3030abd 100644
--- a/spec/models/thing_spec.rb
+++ b/spec/models/thing_spec.rb
@@ -4,10 +4,10 @@ require "spec_helper"
RSpec.describe Thing, type: :model do
describe "#versions", versioning: true do
- let(:thing) { Thing.create! }
+ let(:thing) { described_class.create! }
it "applies the scope option" do
- expect(Thing.reflect_on_association(:versions).scope).to be_a Proc
+ expect(described_class.reflect_on_association(:versions).scope).to be_a Proc
expect(thing.versions.to_sql).to end_with "ORDER BY id desc"
end
diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb
index d00947b..23bb145 100644
--- a/spec/models/translation_spec.rb
+++ b/spec/models/translation_spec.rb
@@ -3,13 +3,13 @@
require "spec_helper"
RSpec.describe Translation, type: :model, versioning: true do
- context "for non-US translations" do
+ context "with non-US translations" do
it "not change the number of versions" do
described_class.create!(headline: "Headline")
expect(PaperTrail::Version.count).to(eq(0))
end
- context "after update" do
+ context "when after update" do
it "not change the number of versions" do
translation = described_class.create!(headline: "Headline")
translation.update(content: "Content")
@@ -17,21 +17,29 @@ RSpec.describe Translation, type: :model, versioning: true do
end
end
- context "after destroy" do
+ context "when after destroy" do
it "not change the number of versions" do
translation = described_class.create!(headline: "Headline")
translation.destroy
expect(PaperTrail::Version.count).to(eq(0))
end
end
+
+ context "when after touch" do
+ it "not change the number of versions" do
+ translation = described_class.create!(headline: "Headline")
+ translation.touch
+ expect(PaperTrail::Version.count).to(eq(0))
+ end
+ end
end
- context "for US translations" do
- context "that are drafts" do
+ context "with US translations" do
+ context "with drafts" do
it "creation does not change the number of versions" do
translation = described_class.new(headline: "Headline")
translation.language_code = "US"
- translation.type = "DRAFT"
+ translation.draft_status = "DRAFT"
translation.save!
expect(PaperTrail::Version.count).to(eq(0))
end
@@ -39,27 +47,43 @@ RSpec.describe Translation, type: :model, versioning: true do
it "update does not change the number of versions" do
translation = described_class.new(headline: "Headline")
translation.language_code = "US"
- translation.type = "DRAFT"
+ translation.draft_status = "DRAFT"
translation.save!
translation.update(content: "Content")
expect(PaperTrail::Version.count).to(eq(0))
end
+
+ it "touch does not change the number of versions" do
+ translation = described_class.new(headline: "Headline")
+ translation.language_code = "US"
+ translation.draft_status = "DRAFT"
+ translation.save!
+ translation.touch
+ expect(PaperTrail::Version.count).to(eq(0))
+ end
end
- context "that are not drafts" do
+ context "with non-drafts" do
it "create changes the number of versions" do
described_class.create!(headline: "Headline", language_code: "US")
expect(PaperTrail::Version.count).to(eq(1))
end
- it "update does not change the number of versions" do
+ it "update changes the number of versions" do
translation = described_class.create!(headline: "Headline", language_code: "US")
translation.update(content: "Content")
expect(PaperTrail::Version.count).to(eq(2))
expect(translation.versions.size).to(eq(2))
end
- it "destroy does not change the number of versions" do
+ it "touch changes the number of versions" do
+ translation = described_class.create!(headline: "Headline", language_code: "US")
+ translation.touch
+ expect(PaperTrail::Version.count).to(eq(2))
+ expect(translation.versions.size).to(eq(2))
+ end
+
+ it "destroy changes the number of versions" do
translation = described_class.new(headline: "Headline")
translation.language_code = "US"
translation.save!
diff --git a/spec/models/vegetable_spec.rb b/spec/models/vegetable_spec.rb
new file mode 100644
index 0000000..7ab422a
--- /dev/null
+++ b/spec/models/vegetable_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+require "support/performance_helpers"
+
+if ENV["DB"] == "postgres" && JsonbVersion.table_exists?
+ ::RSpec.describe Vegetable do
+ describe "queries of versions", versioning: true do
+ let!(:vegetable) { described_class.create(name: "Veggie", mass: 1, color: "green") }
+
+ before do
+ vegetable.update(name: "Fidget")
+ vegetable.update(name: "Digit")
+ described_class.create(name: "Cucumber")
+ end
+
+ it "return the vegetable whose name has changed" do
+ result = JsonbVersion.where_attribute_changes(:name).map(&:item)
+ expect(result).to include(vegetable)
+ end
+
+ it "returns the vegetable whose name was Fidget" do
+ result = JsonbVersion.where_object_changes_from({ name: "Fidget" }).map(&:item)
+ expect(result).to include(vegetable)
+ end
+
+ it "returns the vegetable whose name became Digit" do
+ result = JsonbVersion.where_object_changes_to({ name: "Digit" }).map(&:item)
+ expect(result).to include(vegetable)
+ end
+
+ it "returns the vegetable where the object was named Fidget before it changed" do
+ result = JsonbVersion.where_object({ name: "Fidget" }).map(&:item)
+ expect(result).to include(vegetable)
+ end
+
+ it "returns the vegetable that changed to Fidget" do
+ result = JsonbVersion.where_object_changes({ name: "Fidget" }).map(&:item)
+ expect(result).to include(vegetable)
+ end
+ end
+ end
+end
diff --git a/spec/models/version_spec.rb b/spec/models/version_spec.rb
index 761e6e7..4263476 100644
--- a/spec/models/version_spec.rb
+++ b/spec/models/version_spec.rb
@@ -1,14 +1,15 @@
# frozen_string_literal: true
require "spec_helper"
+require "support/shared_examples/queries"
module PaperTrail
::RSpec.describe Version, type: :model do
- describe "object_changes column", versioning: true do
+ describe "#object_changes", versioning: true do
let(:widget) { Widget.create!(name: "Dashboard") }
let(:value) { widget.versions.last.object_changes }
- context "serializer is YAML" do
+ context "when serializer is YAML" do
specify { expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML }
it "store out as a plain hash" do
@@ -42,7 +43,7 @@ module PaperTrail
end
end
- context "serializer is JSON" do
+ context "when serializer is JSON" do
before do
PaperTrail.serializer = PaperTrail::Serializers::JSON
end
@@ -58,13 +59,13 @@ module PaperTrail
end
describe "#paper_trail_originator" do
- context "no previous versions" do
+ context "with no previous versions" do
it "returns nil" do
- expect(PaperTrail::Version.new.paper_trail_originator).to be_nil
+ expect(described_class.new.paper_trail_originator).to be_nil
end
end
- context "has previous version", versioning: true do
+ context "with previous version", versioning: true do
it "returns name of whodunnit" do
name = FFaker::Name.name
widget = Widget.create!(name: FFaker::Name.name)
@@ -76,19 +77,19 @@ module PaperTrail
end
describe "#previous" do
- context "no previous versions" do
+ context "with no previous versions" do
it "returns nil" do
- expect(PaperTrail::Version.new.previous).to be_nil
+ expect(described_class.new.previous).to be_nil
end
end
- context "has previous version", versioning: true do
+ context "with previous version", versioning: true do
it "returns a PaperTrail::Version" do
name = FFaker::Name.name
widget = Widget.create!(name: FFaker::Name.name)
widget.versions.first.update!(whodunnit: name)
widget.update!(name: FFaker::Name.first_name)
- expect(widget.versions.last.previous).to be_instance_of(PaperTrail::Version)
+ expect(widget.versions.last.previous).to be_instance_of(described_class)
end
end
end
@@ -96,261 +97,39 @@ module PaperTrail
describe "#terminator" do
it "is an alias for the `whodunnit` attribute" do
attributes = { whodunnit: FFaker::Name.first_name }
- version = PaperTrail::Version.new(attributes)
+ version = described_class.new(attributes)
expect(version.terminator).to eq(attributes[:whodunnit])
end
end
describe "#version_author" do
it "is an alias for the `terminator` method" do
- version = PaperTrail::Version.new
+ version = described_class.new
expect(version.method(:version_author)).to eq(version.method(:terminator))
end
end
- context "changing the data type of database columns on the fly" do
- # TODO: Changing the data type of these database columns in the middle
- # of the test suite adds a fair amount of complexity. Is there a better
- # way? We already have a `json_versions` table in our tests, maybe we
- # could use that and add a `jsonb_versions` table?
- column_overrides = [false]
- if ENV["DB"] == "postgres"
- column_overrides += %w[json jsonb]
- end
-
- column_overrides.shuffle.each do |column_datatype_override|
- context "with a #{column_datatype_override || 'text'} column" do
- let(:widget) { Widget.new }
- let(:name) { FFaker::Name.first_name }
- let(:int) { column_datatype_override ? 1 : rand(2..6) }
-
- before do
- if column_datatype_override
- ActiveRecord::Base.connection.execute("SAVEPOINT pgtest;")
- %w[object object_changes].each do |column|
- ActiveRecord::Base.connection.execute(
- "ALTER TABLE versions DROP COLUMN #{column};"
- )
- ActiveRecord::Base.connection.execute(
- "ALTER TABLE versions ADD COLUMN #{column} #{column_datatype_override};"
- )
- end
- PaperTrail::Version.reset_column_information
- end
- end
-
- after do
- PaperTrail.serializer = PaperTrail::Serializers::YAML
-
- if column_datatype_override
- ActiveRecord::Base.connection.execute("ROLLBACK TO SAVEPOINT pgtest;")
- PaperTrail::Version.reset_column_information
- end
- end
-
- describe "#where_object", versioning: true do
- it "requires its argument to be a Hash" do
- widget.update!(name: name, an_integer: int)
- widget.update!(name: "foobar", an_integer: 100)
- widget.update!(name: FFaker::Name.last_name, an_integer: 15)
- expect {
- PaperTrail::Version.where_object(:foo)
- }.to raise_error(ArgumentError)
- expect {
- PaperTrail::Version.where_object([])
- }.to raise_error(ArgumentError)
- end
-
- context "YAML serializer" do
- it "locates versions according to their `object` contents" do
- expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML
- widget.update!(name: name, an_integer: int)
- widget.update!(name: "foobar", an_integer: 100)
- widget.update!(name: FFaker::Name.last_name, an_integer: 15)
- expect(
- PaperTrail::Version.where_object(an_integer: int)
- ).to eq([widget.versions[1]])
- expect(
- PaperTrail::Version.where_object(name: name)
- ).to eq([widget.versions[1]])
- expect(
- PaperTrail::Version.where_object(an_integer: 100)
- ).to eq([widget.versions[2]])
- end
- end
-
- context "JSON serializer" do
- it "locates versions according to their `object` contents" do
- PaperTrail.serializer = PaperTrail::Serializers::JSON
- expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON
- widget.update!(name: name, an_integer: int)
- widget.update!(name: "foobar", an_integer: 100)
- widget.update!(name: FFaker::Name.last_name, an_integer: 15)
- expect(
- PaperTrail::Version.where_object(an_integer: int)
- ).to eq([widget.versions[1]])
- expect(
- PaperTrail::Version.where_object(name: name)
- ).to eq([widget.versions[1]])
- expect(
- PaperTrail::Version.where_object(an_integer: 100)
- ).to eq([widget.versions[2]])
- end
- end
- end
-
- describe "#where_object_changes", versioning: true do
- it "requires its argument to be a Hash" do
- expect {
- PaperTrail::Version.where_object_changes(:foo)
- }.to raise_error(ArgumentError)
- expect {
- PaperTrail::Version.where_object_changes([])
- }.to raise_error(ArgumentError)
- end
-
- context "with object_changes_adapter configured" do
- after do
- PaperTrail.config.object_changes_adapter = nil
- end
-
- it "calls the adapter's where_object_changes method" do
- adapter = instance_spy("CustomObjectChangesAdapter")
- bicycle = Bicycle.create!(name: "abc")
- allow(adapter).to(
- receive(:where_object_changes).with(Version, name: "abc")
- ).and_return(bicycle.versions[0..1])
- PaperTrail.config.object_changes_adapter = adapter
- expect(
- bicycle.versions.where_object_changes(name: "abc")
- ).to match_array(bicycle.versions[0..1])
- expect(adapter).to have_received(:where_object_changes)
- end
-
- it "defaults to the original behavior" do
- adapter = Class.new.new
- PaperTrail.config.object_changes_adapter = adapter
- bicycle = Bicycle.create!(name: "abc")
- if column_datatype_override
- expect(
- bicycle.versions.where_object_changes(name: "abc")
- ).to match_array(bicycle.versions[0..1])
- else
- expect do
- bicycle.versions.where_object_changes(name: "abc")
- end.to raise_error(/no longer supports reading YAML/)
- end
- end
- end
-
- # Only test json and jsonb columns. where_object_changes no longer
- # supports text columns.
- if column_datatype_override
- it "locates versions according to their object_changes contents" do
- widget.update!(name: name, an_integer: 0)
- widget.update!(name: "foobar", an_integer: 100)
- widget.update!(name: FFaker::Name.last_name, an_integer: int)
- expect(
- widget.versions.where_object_changes(name: name)
- ).to eq(widget.versions[0..1])
- expect(
- widget.versions.where_object_changes(an_integer: 100)
- ).to eq(widget.versions[1..2])
- expect(
- widget.versions.where_object_changes(an_integer: int)
- ).to eq([widget.versions.last])
- expect(
- widget.versions.where_object_changes(an_integer: 100, name: "foobar")
- ).to eq(widget.versions[1..2])
- end
- else
- it "raises error" do
- expect {
- widget.versions.where_object_changes(name: "foo").to_a
- }.to(raise_error(/no longer supports reading YAML from a text column/))
- end
- end
- end
-
- describe "#where_object_changes_from", versioning: true do
- it "requires its argument to be a Hash" do
- expect {
- PaperTrail::Version.where_object_changes_from(:foo)
- }.to raise_error(ArgumentError)
- expect {
- PaperTrail::Version.where_object_changes_from([])
- }.to raise_error(ArgumentError)
- end
-
- context "with object_changes_adapter configured" do
- after do
- PaperTrail.config.object_changes_adapter = nil
- end
-
- it "calls the adapter's where_object_changes_from method" do
- adapter = instance_spy("CustomObjectChangesAdapter")
- bicycle = Bicycle.create!(name: "abc")
- bicycle.update!(name: "xyz")
-
- allow(adapter).to(
- receive(:where_object_changes_from).with(Version, name: "abc")
- ).and_return([bicycle.versions[1]])
-
- PaperTrail.config.object_changes_adapter = adapter
- expect(
- bicycle.versions.where_object_changes_from(name: "abc")
- ).to match_array([bicycle.versions[1]])
- expect(adapter).to have_received(:where_object_changes_from)
- end
-
- it "defaults to the original behavior" do
- adapter = Class.new.new
- PaperTrail.config.object_changes_adapter = adapter
- bicycle = Bicycle.create!(name: "abc")
- bicycle.update!(name: "xyz")
-
- if column_datatype_override
- expect(
- bicycle.versions.where_object_changes_from(name: "abc")
- ).to match_array([bicycle.versions[1]])
- else
- expect do
- bicycle.versions.where_object_changes_from(name: "abc")
- end.to raise_error(/does not support reading YAML/)
- end
- end
- end
+ context "with text columns", versioning: true do
+ include_examples "queries", :text, ::Widget, :an_integer
+ end
- # Only test json and jsonb columns. where_object_changes_from does
- # not support text columns.
- if column_datatype_override
- it "locates versions according to their object_changes contents" do
- widget.update!(name: name, an_integer: 0)
- widget.update!(name: "foobar", an_integer: 100)
- widget.update!(name: FFaker::Name.last_name, an_integer: int)
+ if ENV["DB"] == "postgres"
+ context "with json columns", versioning: true do
+ include_examples(
+ "queries",
+ :json,
+ ::Fruit, # uses JsonVersion
+ :mass
+ )
+ end
- expect(
- widget.versions.where_object_changes_from(name: name)
- ).to eq([widget.versions[1]])
- expect(
- widget.versions.where_object_changes_from(an_integer: 100)
- ).to eq([widget.versions[2]])
- expect(
- widget.versions.where_object_changes_from(an_integer: int)
- ).to eq([])
- expect(
- widget.versions.where_object_changes_from(an_integer: 100, name: "foobar")
- ).to eq([widget.versions[2]])
- end
- else
- it "raises error" do
- expect {
- widget.versions.where_object_changes_from(name: "foo").to_a
- }.to(raise_error(/does not support reading YAML from a text column/))
- end
- end
- end
- end
+ context "with jsonb columns", versioning: true do
+ include_examples(
+ "queries",
+ :jsonb,
+ ::Vegetable, # uses JsonbVersion
+ :mass
+ )
end
end
end
diff --git a/spec/models/widget_spec.rb b/spec/models/widget_spec.rb
index 409e703..98557af 100644
--- a/spec/models/widget_spec.rb
+++ b/spec/models/widget_spec.rb
@@ -1,15 +1,749 @@
# frozen_string_literal: true
require "spec_helper"
+require "support/performance_helpers"
+
+RSpec.describe Widget, type: :model, versioning: true do
+ describe "#changeset" do
+ it "has expected values" do
+ widget = described_class.create(name: "Henry")
+ changeset = widget.versions.last.changeset
+ expect(changeset["name"]).to eq([nil, "Henry"])
+ expect(changeset["id"]).to eq([nil, widget.id])
+ # When comparing timestamps, round off to the nearest second, because
+ # mysql doesn't do fractional seconds.
+ expect(changeset["created_at"][0]).to be_nil
+ expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
+ expect(changeset["updated_at"][0]).to be_nil
+ expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
+ end
+
+ context "with custom object_changes_adapter" do
+ after do
+ PaperTrail.config.object_changes_adapter = nil
+ end
+
+ it "calls the adapter's load_changeset method" do
+ widget = described_class.create(name: "Henry")
+ adapter = instance_spy("CustomObjectChangesAdapter")
+ PaperTrail.config.object_changes_adapter = adapter
+ allow(adapter).to(
+ receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
+ )
+ changeset = widget.versions.last.changeset
+ expect(changeset[:a]).to eq("b")
+ expect(changeset[:c]).to eq("d")
+ expect(adapter).to have_received(:load_changeset)
+ end
+
+ it "defaults to the original behavior" do
+ adapter = Class.new.new
+ PaperTrail.config.object_changes_adapter = adapter
+ widget = described_class.create(name: "Henry")
+ changeset = widget.versions.last.changeset
+ expect(changeset[:name]).to eq([nil, "Henry"])
+ end
+ end
+ end
+
+ describe "#object_changes_deserialized" do
+ context "when the serializer raises a Psych::DisallowedClass error" do
+ it "prints a warning to stderr" do
+ allow(PaperTrail.serializer).to(
+ receive(:load).and_raise(::Psych::Exception, "kaboom")
+ )
+ widget = described_class.create(name: "Henry")
+ ver = widget.versions.last
+ expect { ver.send(:object_changes_deserialized) }.to(
+ output(/kaboom/).to_stderr
+ )
+ end
+ end
+ end
+
+ context "with a new record" do
+ it "not have any previous versions" do
+ expect(described_class.new.versions).to(eq([]))
+ end
+
+ it "be live" do
+ expect(described_class.new.paper_trail.live?).to(eq(true))
+ end
+ end
+
+ context "with a persisted record" do
+ it "have one previous version" do
+ widget = described_class.create(name: "Henry", created_at: (Time.current - 1.day))
+ expect(widget.versions.length).to(eq(1))
+ end
+
+ it "be nil in its previous version" do
+ widget = described_class.create(name: "Henry")
+ expect(widget.versions.first.object).to(be_nil)
+ expect(widget.versions.first.reify).to(be_nil)
+ end
+
+ it "record the correct event" do
+ widget = described_class.create(name: "Henry")
+ expect(widget.versions.first.event).to(match(/create/i))
+ end
+
+ it "be live" do
+ widget = described_class.create(name: "Henry")
+ expect(widget.paper_trail.live?).to(eq(true))
+ end
+
+ it "use the widget `updated_at` as the version's `created_at`" do
+ widget = described_class.create(name: "Henry")
+ expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
+ end
+
+ context "when updated without any changes" do
+ it "to have two previous versions" do
+ widget = described_class.create(name: "Henry")
+ widget.touch
+ expect(widget.versions.length).to eq(2)
+ end
+ end
+
+ context "when updated with changes" do
+ it "have three previous versions" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ expect(widget.versions.length).to(eq(2))
+ end
+
+ it "be available in its previous version" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ expect(widget.name).to(eq("Harry"))
+ expect(widget.versions.last.object).not_to(be_nil)
+ reified_widget = widget.versions.last.reify
+ expect(reified_widget.name).to(eq("Henry"))
+ expect(widget.name).to(eq("Harry"))
+ end
+
+ it "have the same ID in its previous version" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ expect(widget.versions.last.reify.id).to(eq(widget.id))
+ end
+
+ it "record the correct event" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ expect(widget.versions.last.event).to(match(/update/i))
+ end
+
+ it "have versions that are not live" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.versions.filter_map(&:reify).each do |v|
+ expect(v.paper_trail).not_to be_live
+ end
+ end
+
+ it "have stored changes" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ last_obj_changes = widget.versions.last.object_changes
+ actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
+ (k.to_sym == :updated_at)
+ end
+ expect(actual).to(eq("name" => %w[Henry Harry]))
+ actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
+ expect(actual).to(eq("name" => %w[Henry Harry]))
+ end
+
+ it "return changes with indifferent access" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
+ expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
+ end
+ end
+
+ context "when updated, and has one associated object" do
+ it "not copy the has_one association by default when reifying" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ wotsit = widget.create_wotsit name: "John"
+ reified_widget = widget.versions.last.reify
+ expect(reified_widget.wotsit).to eq(wotsit)
+ expect(widget.reload.wotsit).to eq(wotsit)
+ end
+ end
+
+ context "when updated, and has many associated objects" do
+ it "copy the has_many associations when reifying" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.fluxors.create(name: "f-zero")
+ widget.fluxors.create(name: "f-one")
+ reified_widget = widget.versions.last.reify
+ expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
+ expect(reified_widget.fluxors).to match_array(widget.fluxors)
+ expect(reified_widget.versions.length).to(eq(widget.versions.length))
+ expect(reified_widget.versions).to match_array(widget.versions)
+ end
+ end
+
+ context "when updated, and has many associated polymorphic objects" do
+ it "copy the has_many associations when reifying" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.whatchamajiggers.create(name: "f-zero")
+ widget.whatchamajiggers.create(name: "f-zero")
+ reified_widget = widget.versions.last.reify
+ expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
+ expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
+ expect(reified_widget.versions.length).to(eq(widget.versions.length))
+ expect(reified_widget.versions).to match_array(widget.versions)
+ end
+ end
+
+ context "when updated, polymorphic objects by themselves" do
+ it "not fail with a nil pointer on the polymorphic association" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget = Whatchamajigger.new(name: "f-zero")
+ widget.save!
+ end
+ end
+
+ context "when updated, and then destroyed" do
+ it "record the correct event" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.destroy
+ expect(PaperTrail::Version.last.event).to(match(/destroy/i))
+ end
+
+ it "have three previous versions" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.destroy
+ expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
+ end
+
+ it "returns the expected attributes for the reified widget" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.destroy
+ reified_widget = PaperTrail::Version.last.reify
+ expect(reified_widget.id).to eq(widget.id)
+ expected = widget.attributes
+ actual = reified_widget.attributes
+ expect(expected["id"]).to eq(actual["id"])
+ expect(expected["name"]).to eq(actual["name"])
+ expect(expected["a_text"]).to eq(actual["a_text"])
+ expect(expected["an_integer"]).to eq(actual["an_integer"])
+ expect(expected["a_float"]).to eq(actual["a_float"])
+ expect(expected["a_decimal"]).to eq(actual["a_decimal"])
+ expect(expected["a_datetime"]).to eq(actual["a_datetime"])
+ expect(expected["a_time"]).to eq(actual["a_time"])
+ expect(expected["a_date"]).to eq(actual["a_date"])
+ expect(expected["a_boolean"]).to eq(actual["a_boolean"])
+ expect(expected["type"]).to eq(actual["type"])
+
+ # We are using `to_i` to truncate to the nearest second, but isn't
+ # there still a chance of this failing intermittently if
+ # ___ and ___ occured more than 0.5s apart?
+ expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
+ expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
+ end
+
+ it "be re-creatable from its previous version" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.destroy
+ reified_widget = PaperTrail::Version.last.reify
+ expect(reified_widget.save).to(be_truthy)
+ end
+
+ it "restore its associations on its previous version" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.fluxors.create(name: "flux")
+ widget.destroy
+ reified_widget = PaperTrail::Version.last.reify
+ reified_widget.save
+ expect(reified_widget.fluxors.length).to(eq(1))
+ end
+
+ it "have nil item for last version" do
+ widget = described_class.create(name: "Henry")
+ widget.update(name: "Harry")
+ widget.destroy
+ expect(widget.versions.first.item.object_id).not_to eq(widget.object_id)
+ expect(widget.versions.last.item.object_id).not_to eq(widget.object_id)
+ expect(widget.versions.last.item).to be_nil
+ end
+ end
+ end
+
+ context "with a record's papertrail" do
+ let!(:d0) { Date.new(2009, 5, 29) }
+ let!(:t0) { Time.current }
+ let(:previous_widget) { widget.versions.last.reify }
+ let(:widget) {
+ described_class.create(
+ name: "Warble",
+ a_text: "The quick brown fox",
+ an_integer: 42,
+ a_float: 153.01,
+ a_decimal: 2.71828,
+ a_datetime: t0,
+ a_time: t0,
+ a_date: d0,
+ a_boolean: true
+ )
+ }
+
+ before do
+ widget.update(
+ name: nil,
+ a_text: nil,
+ an_integer: nil,
+ a_float: nil,
+ a_decimal: nil,
+ a_datetime: nil,
+ a_time: nil,
+ a_date: nil,
+ a_boolean: false
+ )
+ end
+
+ it "handle strings" do
+ expect(previous_widget.name).to(eq("Warble"))
+ end
+
+ it "handle text" do
+ expect(previous_widget.a_text).to(eq("The quick brown fox"))
+ end
+
+ it "handle integers" do
+ expect(previous_widget.an_integer).to(eq(42))
+ end
+
+ it "handle floats" do
+ assert_in_delta(153.01, previous_widget.a_float, 0.001)
+ end
+
+ it "handle decimals" do
+ assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
+ end
+
+ it "handle datetimes" do
+ expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
+ end
+
+ it "handle times (time only, no date)" do
+ format = ->(t) { t.utc.strftime "%H:%M:%S" }
+ expect(format[previous_widget.a_time]).to eq(format[t0])
+ end
+
+ it "handle dates" do
+ expect(previous_widget.a_date).to(eq(d0))
+ end
+
+ it "handle booleans" do
+ expect(previous_widget.a_boolean).to(be_truthy)
+ end
+
+ context "when a column has been removed from the record's schema" do
+ let(:last_version) { widget.versions.last }
+
+ it "reify previous version" do
+ assert_kind_of(described_class, last_version.reify)
+ end
+
+ it "restore all forward-compatible attributes" do
+ reified = last_version.reify
+ expect(reified.name).to(eq("Warble"))
+ expect(reified.a_text).to(eq("The quick brown fox"))
+ expect(reified.an_integer).to(eq(42))
+ assert_in_delta(153.01, reified.a_float, 0.001)
+ assert_in_delta(2.7183, reified.a_decimal, 0.0001)
+ expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
+ format = ->(t) { t.utc.strftime "%H:%M:%S" }
+ expect(format[reified.a_time]).to eq(format[t0])
+ expect(reified.a_date).to(eq(d0))
+ expect(reified.a_boolean).to(be_truthy)
+ end
+ end
+ end
+
+ context "with a record" do
+ context "with PaperTrail globally disabled, when updated" do
+ after { PaperTrail.enabled = true }
+
+ it "not add to its trail" do
+ widget = described_class.create(name: "Zaphod")
+ PaperTrail.enabled = false
+ count = widget.versions.length
+ widget.update(name: "Beeblebrox")
+ expect(widget.versions.length).to(eq(count))
+ end
+ end
+
+ context "with its paper trail turned off, when updated" do
+ after do
+ PaperTrail.request.enable_model(described_class)
+ end
+
+ it "not add to its trail" do
+ widget = described_class.create(name: "Zaphod")
+ PaperTrail.request.disable_model(described_class)
+ count = widget.versions.length
+ widget.update(name: "Beeblebrox")
+ expect(widget.versions.length).to(eq(count))
+ end
+
+ it "add to its trail" do
+ widget = described_class.create(name: "Zaphod")
+ PaperTrail.request.disable_model(described_class)
+ count = widget.versions.length
+ widget.update(name: "Beeblebrox")
+ PaperTrail.request.enable_model(described_class)
+ widget.update(name: "Ford")
+ expect(widget.versions.length).to(eq((count + 1)))
+ end
+ end
+ end
-RSpec.describe Widget, type: :model do
- let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
+ context "with somebody making changes" do
+ context "when a record is created" do
+ it "tracks who made the change" do
+ widget = described_class.new(name: "Fidget")
+ PaperTrail.request.whodunnit = "Alice"
+ widget.save
+ version = widget.versions.last
+ expect(version.whodunnit).to(eq("Alice"))
+ expect(version.paper_trail_originator).to(be_nil)
+ expect(version.terminator).to(eq("Alice"))
+ expect(widget.paper_trail.originator).to(eq("Alice"))
+ end
+ end
+
+ context "when created, then updated" do
+ it "tracks who made the change" do
+ widget = described_class.new(name: "Fidget")
+ PaperTrail.request.whodunnit = "Alice"
+ widget.save
+ PaperTrail.request.whodunnit = "Bob"
+ widget.update(name: "Rivet")
+ version = widget.versions.last
+ expect(version.whodunnit).to(eq("Bob"))
+ expect(version.paper_trail_originator).to(eq("Alice"))
+ expect(version.terminator).to(eq("Bob"))
+ expect(widget.paper_trail.originator).to(eq("Bob"))
+ end
+ end
+
+ context "when created, updated, and destroyed" do
+ it "tracks who made the change" do
+ widget = described_class.new(name: "Fidget")
+ PaperTrail.request.whodunnit = "Alice"
+ widget.save
+ PaperTrail.request.whodunnit = "Bob"
+ widget.update(name: "Rivet")
+ PaperTrail.request.whodunnit = "Charlie"
+ widget.destroy
+ version = PaperTrail::Version.last
+ expect(version.whodunnit).to(eq("Charlie"))
+ expect(version.paper_trail_originator).to(eq("Bob"))
+ expect(version.terminator).to(eq("Charlie"))
+ expect(widget.paper_trail.originator).to(eq("Charlie"))
+ end
+ end
+ end
+
+ context "with an item with versions" do
+ context "when the versions were created over time" do
+ let(:widget) { described_class.create(name: "Widget") }
+ let(:t0) { 2.days.ago }
+ let(:t1) { 1.day.ago }
+ let(:t2) { 1.hour.ago }
+
+ before do
+ widget.update(name: "Fidget")
+ widget.update(name: "Digit")
+ widget.versions[0].update(created_at: t0)
+ widget.versions[1].update(created_at: t1)
+ widget.versions[2].update(created_at: t2)
+ widget.update_attribute(:updated_at, t2)
+ end
+
+ it "return nil for version_at before it was created" do
+ expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
+ end
+
+ it "return how it looked when created for version_at its creation" do
+ expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
+ end
+
+ it "return how it looked before its first update" do
+ expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
+ end
+
+ it "return how it looked after its first update" do
+ expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
+ end
+
+ it "return how it looked before its second update" do
+ expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
+ end
+
+ it "return how it looked after its second update" do
+ expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
+ end
+
+ it "return the current object for version_at after latest update" do
+ expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
+ end
+
+ it "still return a widget when appropriate, when passing timestamp as string" do
+ expect(
+ widget.paper_trail.version_at((t0 + 1.second).to_s).name
+ ).to(eq("Widget"))
+ expect(
+ widget.paper_trail.version_at((t1 + 1.second).to_s).name
+ ).to(eq("Fidget"))
+ expect(
+ widget.paper_trail.version_at((t2 + 1.second).to_s).name
+ ).to(eq("Digit"))
+ end
+ end
+
+ describe ".versions_between" do
+ it "return versions in the time period" do
+ widget = described_class.create(name: "Widget")
+ widget.update(name: "Fidget")
+ widget.update(name: "Digit")
+ widget.versions[0].update(created_at: 30.days.ago)
+ widget.versions[1].update(created_at: 15.days.ago)
+ widget.versions[2].update(created_at: 1.day.ago)
+ widget.update_attribute(:updated_at, 1.day.ago)
+ expect(
+ widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
+ ).to(eq(["Fidget"]))
+ expect(
+ widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
+ ).to(eq(%w[Widget Fidget]))
+ expect(
+ widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
+ ).to(eq(%w[Fidget Digit Digit]))
+ expect(
+ widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
+ ).to(eq([]))
+ end
+ end
+
+ context "with the first version" do
+ let(:widget) { described_class.create(name: "Widget") }
+ let(:version) { widget.versions.last }
+
+ before do
+ widget = described_class.create(name: "Widget")
+ widget.update(name: "Fidget")
+ widget.update(name: "Digit")
+ end
+
+ it "have a nil previous version" do
+ expect(version.previous).to(be_nil)
+ end
+
+ it "return the next version" do
+ expect(version.next).to(eq(widget.versions[1]))
+ end
+
+ it "return the correct index" do
+ expect(version.index).to(eq(0))
+ end
+ end
+
+ context "with the last version" do
+ let(:widget) { described_class.create(name: "Widget") }
+ let(:version) { widget.versions.last }
+
+ before do
+ widget.update(name: "Fidget")
+ widget.update(name: "Digit")
+ end
+
+ it "return the previous version" do
+ expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
+ end
+
+ it "have a nil next version" do
+ expect(version.next).to(be_nil)
+ end
+
+ it "return the correct index" do
+ expect(version.index).to(eq((widget.versions.length - 1)))
+ end
+ end
+ end
+
+ context "with a reified item" do
+ it "know which version it came from, and return its previous self" do
+ widget = described_class.create(name: "Bob")
+ %w[Tom Dick Jane].each do |name|
+ widget.update(name: name)
+ end
+ version = widget.versions.last
+ widget = version.reify
+ expect(widget.version).to(eq(version))
+ expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
+ end
+ end
+
+ describe "#next_version" do
+ context "with a reified item" do
+ it "returns the object (not a Version) as it became next" do
+ widget = described_class.create(name: "Bob")
+ %w[Tom Dick Jane].each do |name|
+ widget.update(name: name)
+ end
+ second_widget = widget.versions[1].reify
+ last_widget = widget.versions.last.reify
+ expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
+ expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
+ end
+ end
+
+ context "with a non-reified item" do
+ it "always returns nil because cannot ever have a next version" do
+ widget = described_class.new
+ expect(widget.paper_trail.next_version).to(be_nil)
+ widget.save
+ %w[Tom Dick Jane].each do |name|
+ widget.update(name: name)
+ end
+ expect(widget.paper_trail.next_version).to(be_nil)
+ end
+ end
+ end
+
+ describe "#previous_version" do
+ context "with a reified item" do
+ it "returns the object (not a Version) as it was most recently" do
+ widget = described_class.create(name: "Bob")
+ %w[Tom Dick Jane].each do |name|
+ widget.update(name: name)
+ end
+ second_widget = widget.versions[1].reify
+ last_widget = widget.versions.last.reify
+ expect(second_widget.paper_trail.previous_version).to(be_nil)
+ expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
+ end
+ end
+
+ context "with a non-reified item" do
+ it "returns the object (not a Version) as it was most recently" do
+ widget = described_class.new
+ expect(widget.paper_trail.previous_version).to(be_nil)
+ widget.save
+ %w[Tom Dick Jane].each do |name|
+ widget.update(name: name)
+ end
+ expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
+ end
+ end
+ end
+
+ context "with an unsaved record" do
+ it "not have a version created on destroy" do
+ widget = described_class.new
+ widget.destroy
+ expect(widget.versions.empty?).to(eq(true))
+ end
+ end
+
+ context "when measuring the memory allocation of" do
+ let(:widget) do
+ described_class.new(
+ name: "Warble",
+ a_text: "The quick brown fox",
+ an_integer: 42,
+ a_float: 153.01,
+ a_decimal: 2.71828,
+ a_boolean: true
+ )
+ end
+
+ before do
+ # Json fields for `object` & `object_changes` attributes is most efficient way
+ # to do the things - this way we will save even more RAM, as well as will skip
+ # the whole YAML serialization
+ allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
+ allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)
+
+ # Force the loading of all lazy things like class definitions,
+ # in order to get the pure benchmark
+ version_building.call
+ end
+
+ describe "#build_version_on_create" do
+ let(:version_building) do
+ lambda do
+ widget.paper_trail.send(
+ :build_version_on_create,
+ in_after_callback: false
+ )
+ end
+ end
+
+ it "is frugal enough" do
+ # Some time ago there was 95kbs..
+ # At the time of commit the test passes with assertion on 17kbs.
+ # Lets assert 20kbs then, to avoid flaky fails.
+ expect(&version_building).to allocate_less_than(20).kilobytes
+ end
+ end
+
+ describe "#build_version_on_update" do
+ let(:widget) do
+ super().tap do |w|
+ w.save!
+ w.attributes = {
+ name: "Dostoyevsky",
+ a_text: "The slow yellow mouse",
+ an_integer: 84,
+ a_float: 306.02,
+ a_decimal: 5.43656,
+ a_boolean: false
+ }
+ end
+ end
+ let(:version_building) do
+ lambda do
+ widget.paper_trail.send(
+ :build_version_on_update,
+ force: false,
+ in_after_callback: false,
+ is_touch: false
+ )
+ end
+ end
+
+ it "is frugal enough" do
+ # Some time ago there was 144kbs..
+ # At the time of commit the test passes with assertion on 27kbs.
+ # Lets assert 35kbs then, to avoid flaky fails.
+ expect(&version_building).to allocate_less_than(35).kilobytes
+ end
+ end
+ end
describe "`be_versioned` matcher" do
it { is_expected.to be_versioned }
end
describe "`have_a_version_with` matcher", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
before do
widget.update!(name: "Leonard", an_integer: 1)
widget.update!(name: "Tom")
@@ -24,20 +758,24 @@ RSpec.describe Widget, type: :model do
end
describe "versioning option" do
- context "enabled", versioning: true do
+ context "when enabled", versioning: true do
it "enables versioning" do
+ widget = described_class.create! name: "Bob", an_integer: 1
expect(widget.versions.size).to eq(1)
end
end
- context "disabled (default)" do
+ context "when disabled", versioning: false do
it "does not enable versioning" do
+ widget = described_class.create! name: "Bob", an_integer: 1
expect(widget.versions.size).to eq(0)
end
end
end
describe "Callbacks", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
describe "before_save" do
it "resets value for timestamp attrs for update so that value gets updated properly" do
widget.update!(name: "Foobar")
@@ -47,7 +785,7 @@ RSpec.describe Widget, type: :model do
end
describe "after_create" do
- let(:widget) { Widget.create!(name: "Foobar", created_at: Time.current - 1.week) }
+ let(:widget) { described_class.create!(name: "Foobar", created_at: Time.current - 1.week) }
it "corresponding version uses the widget's `updated_at`" do
expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
@@ -111,6 +849,8 @@ RSpec.describe Widget, type: :model do
end
describe "Association", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
describe "sort order" do
it "sorts by the timestamp order from the `VersionConcern`" do
expect(widget.versions.to_sql).to eq(
@@ -120,28 +860,20 @@ RSpec.describe Widget, type: :model do
end
end
- if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
- describe "IdentityMap", versioning: true do
- it "does not clobber the IdentityMap when reifying" do
- widget.update name: "Henry", created_at: Time.current - 1.day
- widget.update name: "Harry"
- allow(ActiveRecord::IdentityMap).to receive(:without)
- widget.versions.last.reify
- expect(ActiveRecord::IdentityMap).to have_receive(:without).once
- end
- end
- end
-
describe "#create", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
it "creates a version record" do
- wordget = Widget.create
+ wordget = described_class.create
assert_equal 1, wordget.versions.length
end
end
describe "#destroy", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
it "creates a version record" do
- widget = Widget.create
+ widget = described_class.create
assert_equal 1, widget.versions.length
widget.destroy
versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id)
@@ -154,7 +886,7 @@ RSpec.describe Widget, type: :model do
# the `widget.versions` association, instead of `with_item_keys`.
PaperTrail::Version.with_item_keys("Widget", widget.id)
}
- widget = Widget.create
+ widget = described_class.create
assert_equal 1, widget.versions.length
widget.destroy
assert_equal 2, versions.call(widget).length
@@ -168,6 +900,8 @@ RSpec.describe Widget, type: :model do
end
describe "#paper_trail.originator", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
describe "return value" do
let(:orig_name) { FFaker::Name.name }
let(:new_name) { FFaker::Name.name }
@@ -206,7 +940,9 @@ RSpec.describe Widget, type: :model do
end
describe "#version_at", versioning: true do
- context "Timestamp argument is AFTER object has been destroyed" do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
+ context "when Timestamp argument is AFTER object has been destroyed" do
it "returns nil" do
widget.update_attribute(:name, "foobar")
widget.destroy
@@ -216,37 +952,46 @@ RSpec.describe Widget, type: :model do
end
describe "touch", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
it "creates a version" do
expect { widget.touch }.to change {
widget.versions.count
}.by(+1)
end
- context "request is disabled" do
+ context "when request is disabled" do
it "does not create a version" do
- count = widget.versions.count
PaperTrail.request(enabled: false) do
- widget.touch
+ expect { widget.touch }.not_to(change { widget.versions.count })
end
- expect(count).to eq(count)
end
end
end
describe ".paper_trail.update_columns", versioning: true do
it "creates a version record" do
- widget = Widget.create
+ widget = described_class.create
expect(widget.versions.count).to eq(1)
widget.paper_trail.update_columns(name: "Bugle")
expect(widget.versions.count).to eq(2)
expect(widget.versions.last.event).to(eq("update"))
expect(widget.versions.last.changeset[:name]).to eq([nil, "Bugle"])
end
+
+ it "uses current time for the version created_at" do
+ widget = described_class.create(updated_at: "2015-01-01 15:00")
+ widget.paper_trail.update_columns(name: "Bugle")
+ version = widget.versions.where(event: "update").last
+ expect(version.created_at.to_f).to be_within(5.0).of(Time.now.to_f)
+ end
end
describe "#update", versioning: true do
+ let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
it "creates a version record" do
- widget = Widget.create
+ widget = described_class.create
assert_equal 1, widget.versions.length
widget.update(name: "Bugle")
assert_equal 2, widget.versions.length
diff --git a/spec/models/wotsit_spec.rb b/spec/models/wotsit_spec.rb
new file mode 100644
index 0000000..e72fbef
--- /dev/null
+++ b/spec/models/wotsit_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Wotsit, versioning: true do
+ it "update! records timestamps" do
+ wotsit = described_class.create!(name: "wotsit")
+ wotsit.update!(name: "changed")
+ reified = wotsit.versions.last.reify
+ expect(reified.created_at).not_to(be_nil)
+ expect(reified.updated_at).not_to(be_nil)
+ end
+
+ it "update! does not raise error" do
+ wotsit = described_class.create!(name: "name1")
+ expect { wotsit.update!(name: "name2") }.not_to(raise_error)
+ end
+end
diff --git a/spec/paper_trail/cleaner_spec.rb b/spec/paper_trail/cleaner_spec.rb
index 78755bf..33d954f 100644
--- a/spec/paper_trail/cleaner_spec.rb
+++ b/spec/paper_trail/cleaner_spec.rb
@@ -23,7 +23,7 @@ module PaperTrail
animals.each { |animal| expect(animal.versions.size).to(eq(3)) }
end
- context "no options provided" do
+ context "with no options provided" do
it "removes extra versions for each item" do
PaperTrail.clean_versions!
expect(PaperTrail::Version.count).to(eq(3))
@@ -38,7 +38,7 @@ module PaperTrail
end
end
- context "keeping 2" do
+ context "when keeping 2" do
it "keeps two records, instead of the usual one" do
PaperTrail.clean_versions!(keeping: 2)
expect(PaperTrail::Version.all.count).to(eq(6))
@@ -67,7 +67,7 @@ module PaperTrail
end
context "with the :item_id option" do
- context "single ID received" do
+ context "when a single ID is received" do
it "only deletes the versions for the Item with that ID" do
PaperTrail.clean_versions!(item_id: animal.id)
expect(animal.versions.size).to(eq(1))
@@ -75,7 +75,7 @@ module PaperTrail
end
end
- context "collection of IDs received" do
+ context "when a collection of IDs is received" do
it "only deletes versions for the Item(s) with those IDs" do
PaperTrail.clean_versions!(item_id: [animal.id, dog.id])
expect(animal.versions.size).to(eq(1))
@@ -85,8 +85,8 @@ module PaperTrail
end
end
- context "options combinations" do
- context ":date" do
+ context "with options combinations" do
+ context "with :date" do
before do
[animal, dog].each do |animal|
animal.versions.each do |ver|
@@ -105,7 +105,7 @@ module PaperTrail
end
end
- context "and :keeping" do
+ context "with :keeping" do
it "restrict cleaning properly" do
date = animal.versions.first.created_at.to_date
PaperTrail.clean_versions!(date: date, keeping: 2)
@@ -118,7 +118,7 @@ module PaperTrail
end
end
- context "and :item_id" do
+ context "with :item_id" do
it "restrict cleaning properly" do
date = animal.versions.first.created_at.to_date
PaperTrail.clean_versions!(date: date, item_id: dog.id)
@@ -129,7 +129,7 @@ module PaperTrail
end
end
- context ", :item_id, and :keeping" do
+ context "with :item_id, and :keeping" do
it "restrict cleaning properly" do
date = animal.versions.first.created_at.to_date
PaperTrail.clean_versions!(date: date, item_id: dog.id, keeping: 2)
@@ -141,7 +141,7 @@ module PaperTrail
end
end
- context ":keeping and :item_id" do
+ context "with :keeping and :item_id" do
it "restrict cleaning properly" do
PaperTrail.clean_versions!(keeping: 2, item_id: animal.id)
expect(animal.versions.size).to(eq(2))
diff --git a/spec/paper_trail/compatibility_spec.rb b/spec/paper_trail/compatibility_spec.rb
index ecd2105..79c6597 100644
--- a/spec/paper_trail/compatibility_spec.rb
+++ b/spec/paper_trail/compatibility_spec.rb
@@ -14,7 +14,7 @@ module PaperTrail
context "when incompatible" do
it "writes a warning to stderr" do
- ar_version = ::Gem::Version.new("6.2.0")
+ ar_version = ::Gem::Version.new("7.1.0")
expect {
described_class.check_activerecord(ar_version)
}.to output(/not compatible/).to_stderr
diff --git a/spec/paper_trail/config_spec.rb b/spec/paper_trail/config_spec.rb
index f91d1ba..c2e2eaf 100644
--- a/spec/paper_trail/config_spec.rb
+++ b/spec/paper_trail/config_spec.rb
@@ -52,17 +52,6 @@ module PaperTrail
limited_bike.save
assert_equal 2, limited_bike.versions.length
end
-
- context "when item_subtype column is absent" do
- it "uses global version_limit" do
- PaperTrail.config.version_limit = 6
- names = PaperTrail::Version.column_names - ["item_subtype"]
- allow(PaperTrail::Version).to receive(:column_names).and_return(names)
- bike = LimitedBicycle.create!(name: "My Bike") # has_paper_trail limit: 3
- 10.times { bike.update(name: SecureRandom.hex(8)) }
- assert_equal 7, bike.versions.length
- end
- end
end
end
end
diff --git a/spec/paper_trail/events/base_spec.rb b/spec/paper_trail/events/base_spec.rb
index 90b2e26..fd504ce 100644
--- a/spec/paper_trail/events/base_spec.rb
+++ b/spec/paper_trail/events/base_spec.rb
@@ -6,36 +6,36 @@ module PaperTrail
module Events
::RSpec.describe Base do
describe "#changed_notably?", versioning: true do
- context "new record" do
+ context "with a new record" do
it "returns true" do
g = Gadget.new(created_at: Time.current)
- event = PaperTrail::Events::Base.new(g, false)
+ event = described_class.new(g, false)
expect(event.changed_notably?).to eq(true)
end
end
- context "persisted record without update timestamps" do
+ context "with a persisted record without update timestamps" do
it "only acknowledges non-ignored attrs" do
gadget = Gadget.create!(created_at: Time.current)
gadget.name = "Wrench"
- event = PaperTrail::Events::Base.new(gadget, false)
+ event = described_class.new(gadget, false)
expect(event.changed_notably?).to eq(true)
end
it "does not acknowledge ignored attr (brand)" do
gadget = Gadget.create!(created_at: Time.current)
gadget.brand = "Acme"
- event = PaperTrail::Events::Base.new(gadget, false)
+ event = described_class.new(gadget, false)
expect(event.changed_notably?).to eq(false)
end
end
- context "persisted record with update timestamps" do
+ context "with a persisted record with update timestamps" do
it "only acknowledges non-ignored attrs" do
gadget = Gadget.create!(created_at: Time.current)
gadget.name = "Wrench"
gadget.updated_at = Time.current
- event = PaperTrail::Events::Base.new(gadget, false)
+ event = described_class.new(gadget, false)
expect(event.changed_notably?).to eq(true)
end
@@ -43,7 +43,7 @@ module PaperTrail
gadget = Gadget.create!(created_at: Time.current)
gadget.brand = "Acme"
gadget.updated_at = Time.current
- event = PaperTrail::Events::Base.new(gadget, false)
+ event = described_class.new(gadget, false)
expect(event.changed_notably?).to eq(false)
end
end
@@ -53,7 +53,7 @@ module PaperTrail
it "returns a hash lacking the skipped attribute" do
# Skipper has_paper_trail(..., skip: [:another_timestamp])
skipper = Skipper.create!(another_timestamp: Time.current)
- event = PaperTrail::Events::Base.new(skipper, false)
+ event = described_class.new(skipper, false)
attributes = event.send(:nonskipped_attributes_before_change, false)
expect(attributes).not_to have_key("another_timestamp")
end
diff --git a/spec/paper_trail/events/destroy_spec.rb b/spec/paper_trail/events/destroy_spec.rb
index bfa91f3..f75d3ef 100644
--- a/spec/paper_trail/events/destroy_spec.rb
+++ b/spec/paper_trail/events/destroy_spec.rb
@@ -11,17 +11,21 @@ module PaperTrail
name: "Carter",
path_to_stardom: "Mexican radio"
)
- data = PaperTrail::Events::Destroy.new(carter, true).data
+ data = described_class.new(carter, true).data
expect(data[:item_type]).to eq("Family::Family")
expect(data[:item_subtype]).to eq("Family::CelebrityFamily")
end
- context "skipper" do
+ context "with skipper" do
let(:skipper) { Skipper.create!(another_timestamp: Time.current) }
- let(:data) { PaperTrail::Events::Destroy.new(skipper, false).data }
+ let(:data) { described_class.new(skipper, false).data }
it "includes `object` without skipped attributes" do
- object = YAML.load(data[:object])
+ object = if ::YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load(data[:object])
+ else
+ YAML.load(data[:object])
+ end
expect(object["id"]).to eq(skipper.id)
expect(object).to have_key("updated_at")
expect(object).to have_key("created_at")
@@ -29,7 +33,11 @@ module PaperTrail
end
it "includes `object_changes` without skipped and ignored attributes" do
- changes = YAML.load(data[:object_changes])
+ changes = if ::YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load(data[:object_changes])
+ else
+ YAML.load(data[:object_changes])
+ end
expect(changes["id"]).to eq([skipper.id, nil])
expect(changes["updated_at"][0]).to be_present
expect(changes["updated_at"][1]).to be_nil
diff --git a/spec/paper_trail/events/update_spec.rb b/spec/paper_trail/events/update_spec.rb
index d16564b..06942cd 100644
--- a/spec/paper_trail/events/update_spec.rb
+++ b/spec/paper_trail/events/update_spec.rb
@@ -6,14 +6,14 @@ module PaperTrail
module Events
::RSpec.describe Update do
describe "#data", versioning: true do
- context "is_touch false" do
+ context "when is_touch false" do
it "object_changes is present" do
carter = Family::CelebrityFamily.create(
name: "Carter",
path_to_stardom: "Mexican radio"
)
carter.path_to_stardom = "Johnny"
- data = PaperTrail::Events::Update.new(carter, false, false, nil).data
+ data = described_class.new(carter, false, false, nil).data
expect(data[:object_changes]).to eq(
<<~YAML
---
@@ -25,14 +25,14 @@ module PaperTrail
end
end
- context "is_touch true" do
+ context "when is_touch true" do
it "object_changes is nil" do
carter = Family::CelebrityFamily.create(
name: "Carter",
path_to_stardom: "Mexican radio"
)
carter.path_to_stardom = "Johnny"
- data = PaperTrail::Events::Update.new(carter, false, true, nil).data
+ data = described_class.new(carter, false, true, nil).data
expect(data[:object_changes]).to be_nil
end
end
diff --git a/spec/paper_trail/model_config_spec.rb b/spec/paper_trail/model_config_spec.rb
index ac35914..accb161 100644
--- a/spec/paper_trail/model_config_spec.rb
+++ b/spec/paper_trail/model_config_spec.rb
@@ -8,7 +8,7 @@ module PaperTrail
describe "versions:" do
it "name can be passed instead of an options hash", :deprecated do
allow(::ActiveSupport::Deprecation).to receive(:warn)
- klass = Class.new(ActiveRecord::Base) do
+ klass = Class.new(ApplicationRecord) do
has_paper_trail versions: :drafts
end
expect(klass.reflect_on_association(:drafts)).to be_a(
@@ -21,7 +21,7 @@ module PaperTrail
end
it "name can be passed in the options hash" do
- klass = Class.new(ActiveRecord::Base) do
+ klass = Class.new(ApplicationRecord) do
has_paper_trail versions: { name: :drafts }
end
expect(klass.reflect_on_association(:drafts)).to be_a(
@@ -30,7 +30,7 @@ module PaperTrail
end
it "class_name can be passed in the options hash" do
- klass = Class.new(ActiveRecord::Base) do
+ klass = Class.new(ApplicationRecord) do
has_paper_trail versions: { class_name: "NoObjectVersion" }
end
expect(klass.reflect_on_association(:versions).options[:class_name]).to eq(
@@ -39,7 +39,7 @@ module PaperTrail
end
it "allows any option that has_many supports" do
- klass = Class.new(ActiveRecord::Base) do
+ klass = Class.new(ApplicationRecord) do
has_paper_trail versions: { autosave: true, validate: true }
end
expect(klass.reflect_on_association(:versions).options[:autosave]).to eq true
@@ -47,7 +47,7 @@ module PaperTrail
end
it "can even override options that PaperTrail adds to has_many" do
- klass = Class.new(ActiveRecord::Base) do
+ klass = Class.new(ApplicationRecord) do
has_paper_trail versions: { as: :foo }
end
expect(klass.reflect_on_association(:versions).options[:as]).to eq :foo
@@ -55,7 +55,7 @@ module PaperTrail
it "raises an error on unknown has_many options" do
expect {
- Class.new(ActiveRecord::Base) do
+ Class.new(ApplicationRecord) do
has_paper_trail versions: { read_my_mind: true, validate: true }
end
}.to raise_error(
@@ -66,7 +66,7 @@ module PaperTrail
describe "passing an abstract class to class_name" do
it "raises an error" do
expect {
- Class.new(ActiveRecord::Base) do
+ Class.new(ApplicationRecord) do
has_paper_trail versions: { class_name: "AbstractVersion" }
end
}.to raise_error(
@@ -79,7 +79,7 @@ module PaperTrail
describe "class_name:" do
it "can be used instead of versions: {class_name: ...}", :deprecated do
allow(::ActiveSupport::Deprecation).to receive(:warn)
- klass = Class.new(ActiveRecord::Base) do
+ klass = Class.new(ApplicationRecord) do
has_paper_trail class_name: "NoObjectVersion"
end
expect(klass.reflect_on_association(:versions).options[:class_name]).to eq(
diff --git a/spec/paper_trail/model_spec.rb b/spec/paper_trail/model_spec.rb
deleted file mode 100644
index 1fec082..0000000
--- a/spec/paper_trail/model_spec.rb
+++ /dev/null
@@ -1,922 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-require "support/performance_helpers"
-
-RSpec.describe(::PaperTrail, versioning: true) do
- describe "#changeset" do
- it "has expected values" do
- widget = Widget.create(name: "Henry")
- changeset = widget.versions.last.changeset
- expect(changeset["name"]).to eq([nil, "Henry"])
- expect(changeset["id"]).to eq([nil, widget.id])
- # When comparing timestamps, round off to the nearest second, because
- # mysql doesn't do fractional seconds.
- expect(changeset["created_at"][0]).to be_nil
- expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
- expect(changeset["updated_at"][0]).to be_nil
- expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
- end
-
- context "custom object_changes_adapter" do
- after do
- PaperTrail.config.object_changes_adapter = nil
- end
-
- it "calls the adapter's load_changeset method" do
- widget = Widget.create(name: "Henry")
- adapter = instance_spy("CustomObjectChangesAdapter")
- PaperTrail.config.object_changes_adapter = adapter
- allow(adapter).to(
- receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
- )
- changeset = widget.versions.last.changeset
- expect(changeset[:a]).to eq("b")
- expect(changeset[:c]).to eq("d")
- expect(adapter).to have_received(:load_changeset)
- end
-
- it "defaults to the original behavior" do
- adapter = Class.new.new
- PaperTrail.config.object_changes_adapter = adapter
- widget = Widget.create(name: "Henry")
- changeset = widget.versions.last.changeset
- expect(changeset[:name]).to eq([nil, "Henry"])
- end
- end
- end
-
- context "a new record" do
- it "not have any previous versions" do
- expect(Widget.new.versions).to(eq([]))
- end
-
- it "be live" do
- expect(Widget.new.paper_trail.live?).to(eq(true))
- end
- end
-
- context "a persisted record" do
- it "have one previous version" do
- widget = Widget.create(name: "Henry", created_at: (Time.current - 1.day))
- expect(widget.versions.length).to(eq(1))
- end
-
- it "be nil in its previous version" do
- widget = Widget.create(name: "Henry")
- expect(widget.versions.first.object).to(be_nil)
- expect(widget.versions.first.reify).to(be_nil)
- end
-
- it "record the correct event" do
- widget = Widget.create(name: "Henry")
- expect(widget.versions.first.event).to(match(/create/i))
- end
-
- it "be live" do
- widget = Widget.create(name: "Henry")
- expect(widget.paper_trail.live?).to(eq(true))
- end
-
- it "use the widget `updated_at` as the version's `created_at`" do
- widget = Widget.create(name: "Henry")
- expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
- end
-
- context "and then updated without any changes" do
- it "to have two previous versions" do
- widget = Widget.create(name: "Henry")
- widget.touch
- expect(widget.versions.length).to eq(2)
- end
- end
-
- context "and then updated with changes" do
- it "have three previous versions" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- expect(widget.versions.length).to(eq(2))
- end
-
- it "be available in its previous version" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- expect(widget.name).to(eq("Harry"))
- expect(widget.versions.last.object).not_to(be_nil)
- reified_widget = widget.versions.last.reify
- expect(reified_widget.name).to(eq("Henry"))
- expect(widget.name).to(eq("Harry"))
- end
-
- it "have the same ID in its previous version" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- expect(widget.versions.last.reify.id).to(eq(widget.id))
- end
-
- it "record the correct event" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- expect(widget.versions.last.event).to(match(/update/i))
- end
-
- it "have versions that are not live" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.versions.map(&:reify).compact.each do |v|
- expect(v.paper_trail).not_to be_live
- end
- end
-
- it "have stored changes" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- last_obj_changes = widget.versions.last.object_changes
- actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
- (k.to_sym == :updated_at)
- end
- expect(actual).to(eq("name" => %w[Henry Harry]))
- actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
- expect(actual).to(eq("name" => %w[Henry Harry]))
- end
-
- it "return changes with indifferent access" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
- expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
- end
- end
-
- context "updated, and has one associated object" do
- it "not copy the has_one association by default when reifying" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- wotsit = widget.create_wotsit name: "John"
- reified_widget = widget.versions.last.reify
- expect(reified_widget.wotsit).to eq(wotsit)
- expect(widget.reload.wotsit).to eq(wotsit)
- end
- end
-
- context "updated, and has many associated objects" do
- it "copy the has_many associations when reifying" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.fluxors.create(name: "f-zero")
- widget.fluxors.create(name: "f-one")
- reified_widget = widget.versions.last.reify
- expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
- expect(reified_widget.fluxors).to match_array(widget.fluxors)
- expect(reified_widget.versions.length).to(eq(widget.versions.length))
- expect(reified_widget.versions).to match_array(widget.versions)
- end
- end
-
- context "updated, and has many associated polymorphic objects" do
- it "copy the has_many associations when reifying" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.whatchamajiggers.create(name: "f-zero")
- widget.whatchamajiggers.create(name: "f-zero")
- reified_widget = widget.versions.last.reify
- expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
- expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
- expect(reified_widget.versions.length).to(eq(widget.versions.length))
- expect(reified_widget.versions).to match_array(widget.versions)
- end
- end
-
- context "updated, polymorphic objects by themselves" do
- it "not fail with a nil pointer on the polymorphic association" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget = Whatchamajigger.new(name: "f-zero")
- widget.save!
- end
- end
-
- context "updated, and then destroyed" do
- it "record the correct event" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.destroy
- expect(PaperTrail::Version.last.event).to(match(/destroy/i))
- end
-
- it "have three previous versions" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.destroy
- expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
- end
-
- it "returns the expected attributes for the reified widget" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.destroy
- reified_widget = PaperTrail::Version.last.reify
- expect(reified_widget.id).to eq(widget.id)
- expected = widget.attributes
- actual = reified_widget.attributes
- expect(expected["id"]).to eq(actual["id"])
- expect(expected["name"]).to eq(actual["name"])
- expect(expected["a_text"]).to eq(actual["a_text"])
- expect(expected["an_integer"]).to eq(actual["an_integer"])
- expect(expected["a_float"]).to eq(actual["a_float"])
- expect(expected["a_decimal"]).to eq(actual["a_decimal"])
- expect(expected["a_datetime"]).to eq(actual["a_datetime"])
- expect(expected["a_time"]).to eq(actual["a_time"])
- expect(expected["a_date"]).to eq(actual["a_date"])
- expect(expected["a_boolean"]).to eq(actual["a_boolean"])
- expect(expected["type"]).to eq(actual["type"])
-
- # We are using `to_i` to truncate to the nearest second, but isn't
- # there still a chance of this failing intermittently if
- # ___ and ___ occured more than 0.5s apart?
- expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
- expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
- end
-
- it "be re-creatable from its previous version" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.destroy
- reified_widget = PaperTrail::Version.last.reify
- expect(reified_widget.save).to(be_truthy)
- end
-
- it "restore its associations on its previous version" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.fluxors.create(name: "flux")
- widget.destroy
- reified_widget = PaperTrail::Version.last.reify
- reified_widget.save
- expect(reified_widget.fluxors.length).to(eq(1))
- end
-
- it "have nil item for last version" do
- widget = Widget.create(name: "Henry")
- widget.update(name: "Harry")
- widget.destroy
- expect(widget.versions.last.item).to be_nil
- end
-
- it "has changes" do
- book = Book.create! title: "A"
- changes = YAML.load book.versions.last.attributes["object_changes"]
- expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])
-
- book.update! title: "B"
- changes = YAML.load book.versions.last.attributes["object_changes"]
- expect(changes).to eq("title" => %w[A B])
-
- book.destroy
- changes = YAML.load book.versions.last.attributes["object_changes"]
- expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
- end
- end
- end
-
- context "a record's papertrail" do
- let!(:d0) { Date.new(2009, 5, 29) }
- let!(:t0) { Time.current }
- let(:previous_widget) { widget.versions.last.reify }
- let(:widget) {
- Widget.create(
- name: "Warble",
- a_text: "The quick brown fox",
- an_integer: 42,
- a_float: 153.01,
- a_decimal: 2.71828,
- a_datetime: t0,
- a_time: t0,
- a_date: d0,
- a_boolean: true
- )
- }
-
- before do
- widget.update(
- name: nil,
- a_text: nil,
- an_integer: nil,
- a_float: nil,
- a_decimal: nil,
- a_datetime: nil,
- a_time: nil,
- a_date: nil,
- a_boolean: false
- )
- end
-
- it "handle strings" do
- expect(previous_widget.name).to(eq("Warble"))
- end
-
- it "handle text" do
- expect(previous_widget.a_text).to(eq("The quick brown fox"))
- end
-
- it "handle integers" do
- expect(previous_widget.an_integer).to(eq(42))
- end
-
- it "handle floats" do
- assert_in_delta(153.01, previous_widget.a_float, 0.001)
- end
-
- it "handle decimals" do
- assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
- end
-
- it "handle datetimes" do
- expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
- end
-
- it "handle times (time only, no date)" do
- format = ->(t) { t.utc.strftime "%H:%M:%S" }
- expect(format[previous_widget.a_time]).to eq(format[t0])
- end
-
- it "handle dates" do
- expect(previous_widget.a_date).to(eq(d0))
- end
-
- it "handle booleans" do
- expect(previous_widget.a_boolean).to(be_truthy)
- end
-
- context "after a column is removed from the record's schema" do
- let(:last_version) { widget.versions.last }
-
- it "reify previous version" do
- assert_kind_of(Widget, last_version.reify)
- end
-
- it "restore all forward-compatible attributes" do
- reified = last_version.reify
- expect(reified.name).to(eq("Warble"))
- expect(reified.a_text).to(eq("The quick brown fox"))
- expect(reified.an_integer).to(eq(42))
- assert_in_delta(153.01, reified.a_float, 0.001)
- assert_in_delta(2.7183, reified.a_decimal, 0.0001)
- expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
- format = ->(t) { t.utc.strftime "%H:%M:%S" }
- expect(format[reified.a_time]).to eq(format[t0])
- expect(reified.a_date).to(eq(d0))
- expect(reified.a_boolean).to(be_truthy)
- end
- end
- end
-
- context "A record" do
- context "with PaperTrail globally disabled, when updated" do
- after { PaperTrail.enabled = true }
-
- it "not add to its trail" do
- widget = Widget.create(name: "Zaphod")
- PaperTrail.enabled = false
- count = widget.versions.length
- widget.update(name: "Beeblebrox")
- expect(widget.versions.length).to(eq(count))
- end
- end
-
- context "with its paper trail turned off, when updated" do
- after do
- PaperTrail.request.enable_model(Widget)
- end
-
- it "not add to its trail" do
- widget = Widget.create(name: "Zaphod")
- PaperTrail.request.disable_model(Widget)
- count = widget.versions.length
- widget.update(name: "Beeblebrox")
- expect(widget.versions.length).to(eq(count))
- end
-
- it "add to its trail" do
- widget = Widget.create(name: "Zaphod")
- PaperTrail.request.disable_model(Widget)
- count = widget.versions.length
- widget.update(name: "Beeblebrox")
- PaperTrail.request.enable_model(Widget)
- widget.update(name: "Ford")
- expect(widget.versions.length).to(eq((count + 1)))
- end
- end
- end
-
- context "A papertrail with somebody making changes" do
- context "when a record is created" do
- it "tracks who made the change" do
- widget = Widget.new(name: "Fidget")
- PaperTrail.request.whodunnit = "Alice"
- widget.save
- version = widget.versions.last
- expect(version.whodunnit).to(eq("Alice"))
- expect(version.paper_trail_originator).to(be_nil)
- expect(version.terminator).to(eq("Alice"))
- expect(widget.paper_trail.originator).to(eq("Alice"))
- end
- end
-
- context "when created, then updated" do
- it "tracks who made the change" do
- widget = Widget.new(name: "Fidget")
- PaperTrail.request.whodunnit = "Alice"
- widget.save
- PaperTrail.request.whodunnit = "Bob"
- widget.update(name: "Rivet")
- version = widget.versions.last
- expect(version.whodunnit).to(eq("Bob"))
- expect(version.paper_trail_originator).to(eq("Alice"))
- expect(version.terminator).to(eq("Bob"))
- expect(widget.paper_trail.originator).to(eq("Bob"))
- end
- end
-
- context "when created, updated, and destroyed" do
- it "tracks who made the change" do
- widget = Widget.new(name: "Fidget")
- PaperTrail.request.whodunnit = "Alice"
- widget.save
- PaperTrail.request.whodunnit = "Bob"
- widget.update(name: "Rivet")
- PaperTrail.request.whodunnit = "Charlie"
- widget.destroy
- version = PaperTrail::Version.last
- expect(version.whodunnit).to(eq("Charlie"))
- expect(version.paper_trail_originator).to(eq("Bob"))
- expect(version.terminator).to(eq("Charlie"))
- expect(widget.paper_trail.originator).to(eq("Charlie"))
- end
- end
- end
-
- it "update! records timestamps" do
- wotsit = Wotsit.create!(name: "wotsit")
- wotsit.update!(name: "changed")
- reified = wotsit.versions.last.reify
- expect(reified.created_at).not_to(be_nil)
- expect(reified.updated_at).not_to(be_nil)
- end
-
- it "update! does not raise error" do
- wotsit = Wotsit.create!(name: "name1")
- expect { wotsit.update!(name: "name2") }.not_to(raise_error)
- end
-
- context "A subclass" do
- let(:foo) { FooWidget.create }
-
- before do
- foo.update!(name: "Foo")
- end
-
- it "reify with the correct type" do
- expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
- expect(PaperTrail::Version.last.next).to(be_nil)
- end
-
- it "returns the correct originator" do
- PaperTrail.request.whodunnit = "Ben"
- foo.update_attribute(:name, "Geoffrey")
- expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
- end
-
- context "when destroyed" do
- before { foo.destroy }
-
- it "reify with the correct type" do
- assert_kind_of(FooWidget, foo.versions.last.reify)
- expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
- expect(PaperTrail::Version.last.next).to(be_nil)
- end
- end
- end
-
- context "An item with versions" do
- context "which were created over time" do
- let(:widget) { Widget.create(name: "Widget") }
- let(:t0) { 2.days.ago }
- let(:t1) { 1.day.ago }
- let(:t2) { 1.hour.ago }
-
- before do
- widget.update(name: "Fidget")
- widget.update(name: "Digit")
- widget.versions[0].update(created_at: t0)
- widget.versions[1].update(created_at: t1)
- widget.versions[2].update(created_at: t2)
- widget.update_attribute(:updated_at, t2)
- end
-
- it "return nil for version_at before it was created" do
- expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
- end
-
- it "return how it looked when created for version_at its creation" do
- expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
- end
-
- it "return how it looked before its first update" do
- expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
- end
-
- it "return how it looked after its first update" do
- expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
- end
-
- it "return how it looked before its second update" do
- expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
- end
-
- it "return how it looked after its second update" do
- expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
- end
-
- it "return the current object for version_at after latest update" do
- expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
- end
-
- it "still return a widget when appropriate, when passing timestamp as string" do
- expect(
- widget.paper_trail.version_at((t0 + 1.second).to_s).name
- ).to(eq("Widget"))
- expect(
- widget.paper_trail.version_at((t1 + 1.second).to_s).name
- ).to(eq("Fidget"))
- expect(
- widget.paper_trail.version_at((t2 + 1.second).to_s).name
- ).to(eq("Digit"))
- end
- end
-
- describe ".versions_between" do
- it "return versions in the time period" do
- widget = Widget.create(name: "Widget")
- widget.update(name: "Fidget")
- widget.update(name: "Digit")
- widget.versions[0].update(created_at: 30.days.ago)
- widget.versions[1].update(created_at: 15.days.ago)
- widget.versions[2].update(created_at: 1.day.ago)
- widget.update_attribute(:updated_at, 1.day.ago)
- expect(
- widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
- ).to(eq(["Fidget"]))
- expect(
- widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
- ).to(eq(%w[Widget Fidget]))
- expect(
- widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
- ).to(eq(%w[Fidget Digit Digit]))
- expect(
- widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
- ).to(eq([]))
- end
- end
-
- context "on the first version" do
- let(:widget) { Widget.create(name: "Widget") }
- let(:version) { widget.versions.last }
-
- before do
- widget = Widget.create(name: "Widget")
- widget.update(name: "Fidget")
- widget.update(name: "Digit")
- end
-
- it "have a nil previous version" do
- expect(version.previous).to(be_nil)
- end
-
- it "return the next version" do
- expect(version.next).to(eq(widget.versions[1]))
- end
-
- it "return the correct index" do
- expect(version.index).to(eq(0))
- end
- end
-
- context "on the last version" do
- let(:widget) { Widget.create(name: "Widget") }
- let(:version) { widget.versions.last }
-
- before do
- widget.update(name: "Fidget")
- widget.update(name: "Digit")
- end
-
- it "return the previous version" do
- expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
- end
-
- it "have a nil next version" do
- expect(version.next).to(be_nil)
- end
-
- it "return the correct index" do
- expect(version.index).to(eq((widget.versions.length - 1)))
- end
- end
- end
-
- context "An item" do
- let(:article) { Article.new(title: initial_title) }
- let(:initial_title) { "Foobar" }
-
- context "which is created" do
- before { article.save }
-
- it "store fixed meta data" do
- expect(article.versions.last.answer).to(eq(42))
- end
-
- it "store dynamic meta data which is independent of the item" do
- expect(article.versions.last.question).to(eq("31 + 11 = 42"))
- end
-
- it "store dynamic meta data which depends on the item" do
- expect(article.versions.last.article_id).to(eq(article.id))
- end
-
- it "store dynamic meta data based on a method of the item" do
- expect(article.versions.last.action).to(eq(article.action_data_provider_method))
- end
-
- it "store dynamic meta data based on an attribute of the item at creation" do
- expect(article.versions.last.title).to(eq(initial_title))
- end
- end
-
- context "created, then updated" do
- before do
- article.save
- article.update!(content: "Better text.", title: "Rhubarb")
- end
-
- it "store fixed meta data" do
- expect(article.versions.last.answer).to(eq(42))
- end
-
- it "store dynamic meta data which is independent of the item" do
- expect(article.versions.last.question).to(eq("31 + 11 = 42"))
- end
-
- it "store dynamic meta data which depends on the item" do
- expect(article.versions.last.article_id).to(eq(article.id))
- end
-
- it "store dynamic meta data based on an attribute of the item prior to the update" do
- expect(article.versions.last.title).to(eq(initial_title))
- end
- end
-
- context "created, then destroyed" do
- before do
- article.save
- article.destroy
- end
-
- it "store fixed metadata" do
- expect(article.versions.last.answer).to(eq(42))
- end
-
- it "store dynamic metadata which is independent of the item" do
- expect(article.versions.last.question).to(eq("31 + 11 = 42"))
- end
-
- it "store dynamic metadata which depends on the item" do
- expect(article.versions.last.article_id).to(eq(article.id))
- end
-
- it "store dynamic metadata based on attribute of item prior to destruction" do
- expect(article.versions.last.title).to(eq(initial_title))
- end
- end
- end
-
- context "A reified item" do
- it "know which version it came from, and return its previous self" do
- widget = Widget.create(name: "Bob")
- %w[Tom Dick Jane].each do |name|
- widget.update(name: name)
- end
- version = widget.versions.last
- widget = version.reify
- expect(widget.version).to(eq(version))
- expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
- end
- end
-
- describe "#next_version" do
- context "a reified item" do
- it "returns the object (not a Version) as it became next" do
- widget = Widget.create(name: "Bob")
- %w[Tom Dick Jane].each do |name|
- widget.update(name: name)
- end
- second_widget = widget.versions[1].reify
- last_widget = widget.versions.last.reify
- expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
- expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
- end
- end
-
- context "a non-reified item" do
- it "always returns nil because cannot ever have a next version" do
- widget = Widget.new
- expect(widget.paper_trail.next_version).to(be_nil)
- widget.save
- %w[Tom Dick Jane].each do |name|
- widget.update(name: name)
- end
- expect(widget.paper_trail.next_version).to(be_nil)
- end
- end
- end
-
- describe "#previous_version" do
- context "a reified item" do
- it "returns the object (not a Version) as it was most recently" do
- widget = Widget.create(name: "Bob")
- %w[Tom Dick Jane].each do |name|
- widget.update(name: name)
- end
- second_widget = widget.versions[1].reify
- last_widget = widget.versions.last.reify
- expect(second_widget.paper_trail.previous_version).to(be_nil)
- expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
- end
- end
-
- context "a non-reified item" do
- it "returns the object (not a Version) as it was most recently" do
- widget = Widget.new
- expect(widget.paper_trail.previous_version).to(be_nil)
- widget.save
- %w[Tom Dick Jane].each do |name|
- widget.update(name: name)
- end
- expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
- end
- end
- end
-
- context ":has_many :through" do
- it "store version on source <<" do
- book = Book.create(title: "War and Peace")
- dostoyevsky = Person.create(name: "Dostoyevsky")
- Person.create(name: "Solzhenitsyn")
- count = PaperTrail::Version.count
- (book.authors << dostoyevsky)
- expect((PaperTrail::Version.count - count)).to(eq(1))
- expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
- end
-
- it "store version on source create" do
- book = Book.create(title: "War and Peace")
- Person.create(name: "Dostoyevsky")
- Person.create(name: "Solzhenitsyn")
- count = PaperTrail::Version.count
- book.authors.create(name: "Tolstoy")
- expect((PaperTrail::Version.count - count)).to(eq(2))
- expect(
- [PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
- ).to match_array([Person.last, Authorship.last])
- end
-
- it "store version on join destroy" do
- book = Book.create(title: "War and Peace")
- dostoyevsky = Person.create(name: "Dostoyevsky")
- Person.create(name: "Solzhenitsyn")
- (book.authors << dostoyevsky)
- count = PaperTrail::Version.count
- book.authorships.reload.last.destroy
- expect((PaperTrail::Version.count - count)).to(eq(1))
- expect(PaperTrail::Version.last.reify.book).to(eq(book))
- expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
- end
-
- it "store version on join clear" do
- book = Book.create(title: "War and Peace")
- dostoyevsky = Person.create(name: "Dostoyevsky")
- Person.create(name: "Solzhenitsyn")
- book.authors << dostoyevsky
- count = PaperTrail::Version.count
- book.authorships.reload.destroy_all
- expect((PaperTrail::Version.count - count)).to(eq(1))
- expect(PaperTrail::Version.last.reify.book).to(eq(book))
- expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
- end
- end
-
- context "the default accessor, length=, is overwritten" do
- it "returns overwritten value on reified instance" do
- song = Song.create(length: 4)
- song.update(length: 5)
- expect(song.length).to(eq(5))
- expect(song.versions.last.reify.length).to(eq(4))
- end
- end
-
- context "song name is a virtual attribute (no such db column)" do
- it "returns overwritten virtual attribute on the reified instance" do
- song = Song.create(length: 4)
- song.update(length: 5)
- song.name = "Good Vibrations"
- song.save
- song.name = "Yellow Submarine"
- expect(song.name).to(eq("Yellow Submarine"))
- expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
- end
- end
-
- context "An unsaved record" do
- it "not have a version created on destroy" do
- widget = Widget.new
- widget.destroy
- expect(widget.versions.empty?).to(eq(true))
- end
- end
-
- context "Memory allocation of" do
- let(:widget) do
- Widget.new(
- name: "Warble",
- a_text: "The quick brown fox",
- an_integer: 42,
- a_float: 153.01,
- a_decimal: 2.71828,
- a_boolean: true
- )
- end
-
- before do
- # Json fields for `object` & `object_changes` attributes is most efficient way
- # to do the things - this way we will save even more RAM, as well as will skip
- # the whole YAML serialization
- allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
- allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)
-
- # Force the loading of all lazy things like class definitions,
- # in order to get the pure benchmark
- version_building.call
- end
-
- describe "#build_version_on_create" do
- let(:version_building) do
- lambda do
- widget.paper_trail.send(
- :build_version_on_create,
- in_after_callback: false
- )
- end
- end
-
- it "is frugal enough" do
- # Some time ago there was 95kbs..
- # At the time of commit the test passes with assertion on 17kbs.
- # Lets assert 20kbs then, to avoid flaky fails.
- expect(&version_building).to allocate_less_than(20).kilobytes
- end
- end
-
- describe "#build_version_on_update" do
- let(:widget) do
- super().tap do |w|
- w.save!
- w.attributes = {
- name: "Dostoyevsky",
- a_text: "The slow yellow mouse",
- an_integer: 84,
- a_float: 306.02,
- a_decimal: 5.43656,
- a_boolean: false
- }
- end
- end
- let(:version_building) do
- lambda do
- widget.paper_trail.send(
- :build_version_on_update,
- force: false,
- in_after_callback: false,
- is_touch: false
- )
- end
- end
-
- it "is frugal enough" do
- # Some time ago there was 144kbs..
- # At the time of commit the test passes with assertion on 27kbs.
- # Lets assert 35kbs then, to avoid flaky fails.
- expect(&version_building).to allocate_less_than(35).kilobytes
- end
- end
- end
-end
diff --git a/spec/paper_trail/request_spec.rb b/spec/paper_trail/request_spec.rb
index d24fd48..5e2dae5 100644
--- a/spec/paper_trail/request_spec.rb
+++ b/spec/paper_trail/request_spec.rb
@@ -103,8 +103,8 @@ module PaperTrail
end
describe ".with" do
- context "block given" do
- context "all allowed options" do
+ context "with a block given" do
+ context "with all allowed options" do
it "sets options only for the block passed" do
described_class.whodunnit = "some_whodunnit"
described_class.enabled_for_model(Widget, true)
@@ -132,7 +132,7 @@ module PaperTrail
end
end
- context "some invalid options" do
+ context "with some invalid options" do
it "raises an invalid option error" do
subject = proc do
described_class.with(whodunnit: "blah", invalid_option: "foo") do
@@ -140,13 +140,13 @@ module PaperTrail
end
end
- expect { subject.call }.to raise_error(PaperTrail::Request::InvalidOption) do |e|
+ expect { subject.call }.to raise_error(PaperTrail::InvalidOption) do |e|
expect(e.message).to eq "Invalid option: invalid_option"
end
end
end
- context "all invalid options" do
+ context "with all invalid options" do
it "raises an invalid option error" do
subject = proc do
described_class.with(invalid_option: "foo", other_invalid_option: "blah") do
@@ -154,7 +154,7 @@ module PaperTrail
end
end
- expect { subject.call }.to raise_error(PaperTrail::Request::InvalidOption) do |e|
+ expect { subject.call }.to raise_error(PaperTrail::InvalidOption) do |e|
expect(e.message).to eq "Invalid option: invalid_option"
end
end
diff --git a/spec/paper_trail/serializer_spec.rb b/spec/paper_trail/serializer_spec.rb
index 3ee7136..227dc9e 100644
--- a/spec/paper_trail/serializer_spec.rb
+++ b/spec/paper_trail/serializer_spec.rb
@@ -3,8 +3,8 @@
require "spec_helper"
require "support/custom_json_serializer"
-RSpec.describe(PaperTrail, versioning: true) do
- context "YAML serializer" do
+RSpec.describe("PaperTrail serializers", versioning: true) do
+ context "with YAML serializer" do
it "saves the expected YAML in the object column" do
customer = Customer.create(name: "Some text.")
original_attributes = PaperTrail::Events::Base.
@@ -19,7 +19,7 @@ RSpec.describe(PaperTrail, versioning: true) do
end
end
- context "JSON Serializer" do
+ context "with JSON Serializer" do
before do
PaperTrail.configure do |config|
config.serializer = PaperTrail::Serializers::JSON
@@ -55,7 +55,7 @@ RSpec.describe(PaperTrail, versioning: true) do
end
end
- context "Custom Serializer" do
+ context "with Custom Serializer" do
before do
PaperTrail.configure { |config| config.serializer = CustomJsonSerializer }
end
diff --git a/spec/paper_trail/serializers/json_spec.rb b/spec/paper_trail/serializers/json_spec.rb
index 2608369..b8f0fcd 100644
--- a/spec/paper_trail/serializers/json_spec.rb
+++ b/spec/paper_trail/serializers/json_spec.rb
@@ -59,14 +59,6 @@ module PaperTrail
end
end
end
-
- describe ".where_object_changes_condition" do
- it "raises error" do
- expect {
- described_class.where_object_changes_condition
- }.to raise_error(/no longer supports/)
- end
- end
end
end
end
diff --git a/spec/paper_trail/serializers/yaml_spec.rb b/spec/paper_trail/serializers/yaml_spec.rb
index 92ef85a..31fa453 100644
--- a/spec/paper_trail/serializers/yaml_spec.rb
+++ b/spec/paper_trail/serializers/yaml_spec.rb
@@ -22,6 +22,24 @@ module PaperTrail
expect(described_class.load(hash.to_yaml)).to eq(hash)
expect(described_class.load(array.to_yaml)).to eq(array)
end
+
+ it "calls the expected load method based on Psych version" do
+ # `use_yaml_unsafe_load` was added in 5.2.8.1, 6.0.5.1, 6.1.6.1, and 7.0.3.1
+ if rails_supports_safe_load?
+ allow(::YAML).to receive(:safe_load)
+ described_class.load("string")
+ expect(::YAML).to have_received(:safe_load)
+ # Psych 4+ implements .unsafe_load
+ elsif ::YAML.respond_to?(:unsafe_load)
+ allow(::YAML).to receive(:unsafe_load)
+ described_class.load("string")
+ expect(::YAML).to have_received(:unsafe_load)
+ else # Psych < 4
+ allow(::YAML).to receive(:load)
+ described_class.load("string")
+ expect(::YAML).to have_received(:load)
+ end
+ end
end
describe ".dump" do
@@ -42,6 +60,16 @@ module PaperTrail
expect(arel_value(matches.right)).to eq("%\narg1: Val 1\n%")
end
end
+
+ private
+
+ def rails_supports_safe_load?
+ # Rails 7.0.3.1 onwards will always support YAML safe loading
+ return true if ::ActiveRecord.gem_version >= Gem::Version.new("7.0.3.1")
+
+ # Older Rails versions may or may not, depending on whether they have been patched.
+ defined?(ActiveRecord::Base.use_yaml_unsafe_load)
+ end
end
end
end
diff --git a/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb b/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb
new file mode 100644
index 0000000..4bec2e8
--- /dev/null
+++ b/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module PaperTrail
+ module TypeSerializers
+ ::RSpec.describe PostgresArraySerializer do
+ let(:word_array) { [].fill(0, rand(4..8)) { ::FFaker::Lorem.word } }
+ let(:word_array_as_string) { word_array.join("|") }
+
+ let(:the_thing) { described_class.new("foo", "bar") }
+
+ describe ".deserialize" do
+ it "deserializes array to Ruby" do
+ expect(the_thing.deserialize(word_array)).to eq(word_array)
+ end
+
+ it "deserializes string to Ruby array" do
+ allow(the_thing).to receive(:deserialize_with_ar).and_return(word_array)
+ expect(the_thing.deserialize(word_array_as_string)).to eq(word_array)
+ expect(the_thing).to have_received(:deserialize_with_ar)
+ end
+ end
+
+ describe ".dump" do
+ it "serializes Ruby to JSON" do
+ expect(the_thing.serialize(word_array)).to eq(word_array)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/paper_trail/version_limit_spec.rb b/spec/paper_trail/version_limit_spec.rb
index f981b8a..4899ff5 100644
--- a/spec/paper_trail/version_limit_spec.rb
+++ b/spec/paper_trail/version_limit_spec.rb
@@ -21,6 +21,21 @@ module PaperTrail
# 4 versions = 3 updates + 1 create.
end
+ it "cleans up old versions with limit specified on base class" do
+ PaperTrail.config.version_limit = 10
+
+ Animal.paper_trail_options[:limit] = 5
+ Dog.paper_trail_options = Animal.paper_trail_options.without(:limit)
+
+ dog = Dog.create(name: "Fluffy") # Dog specified has_paper_trail with no limit option
+
+ 15.times do |i|
+ dog.update(name: "Name #{i}")
+ end
+ expect(Dog.find(dog.id).versions.count).to eq(6) # Dog uses limit option on base class, Animal
+ # 6 versions = 5 updates + 1 create.
+ end
+
it "cleans up old versions" do
PaperTrail.config.version_limit = 10
widget = Widget.create
diff --git a/spec/paper_trail/version_spec.rb b/spec/paper_trail/version_spec.rb
index e29008b..749a144 100644
--- a/spec/paper_trail/version_spec.rb
+++ b/spec/paper_trail/version_spec.rb
@@ -40,7 +40,7 @@ module PaperTrail
end
describe ".subsequent" do
- context "given a timestamp" do
+ context "with a timestamp" do
it "returns all versions that were created after the timestamp" do
animal = Animal.create
2.times do
@@ -54,7 +54,7 @@ module PaperTrail
end
end
- context "given a Version" do
+ context "with a Version" do
it "grab the timestamp from the version and use that as the value" do
animal = Animal.create
2.times do
@@ -68,7 +68,7 @@ module PaperTrail
end
describe ".preceding" do
- context "given a timestamp" do
+ context "with a timestamp" do
it "returns all versions that were created before the timestamp" do
animal = Animal.create
2.times do
@@ -82,7 +82,7 @@ module PaperTrail
end
end
- context "given a Version" do
+ context "with a Version" do
it "grab the timestamp from the version and use that as the value" do
animal = Animal.create
2.times do
diff --git a/spec/paper_trail_spec.rb b/spec/paper_trail_spec.rb
index 68d71f1..5ff6d9a 100644
--- a/spec/paper_trail_spec.rb
+++ b/spec/paper_trail_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe PaperTrail do
end
end
- context "default" do
+ context "when default" do
it "has versioning off by default" do
expect(described_class).not_to be_enabled
end
@@ -63,7 +63,7 @@ RSpec.describe PaperTrail do
expect(described_class).not_to be_enabled
end
- context "error within `with_versioning` block" do
+ context "when error within `with_versioning` block" do
it "reverts the value of `PaperTrail.enabled?` to its previous state" do
expect(described_class).not_to be_enabled
expect { with_versioning { raise } }.to raise_error(RuntimeError)
@@ -72,7 +72,7 @@ RSpec.describe PaperTrail do
end
end
- context "`versioning: true`", versioning: true do
+ context "with `versioning: true`", versioning: true do
it "has versioning on by default" do
expect(described_class).to be_enabled
end
@@ -86,7 +86,7 @@ RSpec.describe PaperTrail do
end
end
- context "`with_versioning` block at class level" do
+ context "with `with_versioning` block at class level" do
it { expect(described_class).not_to be_enabled }
with_versioning do
diff --git a/spec/requests/articles_spec.rb b/spec/requests/articles_spec.rb
index 2d77ca0..9bffdd3 100644
--- a/spec/requests/articles_spec.rb
+++ b/spec/requests/articles_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe "Articles management", type: :request, order: :defined do
let(:valid_params) { { article: { title: "Doh", content: FFaker::Lorem.sentence } } }
- context "versioning disabled" do
+ context "with versioning disabled" do
specify { expect(PaperTrail).not_to be_enabled }
it "does not create a version" do
@@ -19,7 +19,7 @@ RSpec.describe "Articles management", type: :request, order: :defined do
with_versioning do
let(:article) { Article.last }
- context "`current_user` method returns a `String`" do
+ context "when `current_user` method returns a `String`" do
it "sets that value as the `whodunnit`" do
expect {
post articles_path, params: valid_params
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 63b43ec..30484b2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,12 @@
ENV["RAILS_ENV"] ||= "test"
ENV["DB"] ||= "sqlite"
+require "simplecov"
+SimpleCov.start do
+ add_filter %w[Appraisals Gemfile Rakefile doc gemfiles spec]
+end
+SimpleCov.minimum_coverage(ENV["DB"] == "postgres" ? 96.8 : 92.4)
+
require "byebug"
require_relative "support/pt_arel_helpers"
@@ -47,7 +53,7 @@ end
# in `dummy_app/config/*`. By consolidating it here,
#
# - It can better be understood, and documented in one place
-# - It can more closely resememble a conventional app boot. For example, loading
+# - It can more closely resemble a conventional app boot. For example, loading
# gems (like rspec-rails) _before_ loading the app.
# First, `config/boot.rb` would add gems to $LOAD_PATH.
diff --git a/spec/support/custom_object_changes_adapter.rb b/spec/support/custom_object_changes_adapter.rb
index 419bfe6..19148a5 100644
--- a/spec/support/custom_object_changes_adapter.rb
+++ b/spec/support/custom_object_changes_adapter.rb
@@ -10,6 +10,10 @@ class CustomObjectChangesAdapter
version.changeset
end
+ def where_attribute_changes(klass, attribute)
+ klass.where(attribute)
+ end
+
def where_object_changes(klass, attributes)
klass.where(attributes)
end
@@ -17,4 +21,8 @@ class CustomObjectChangesAdapter
def where_object_changes_from(klass, attributes)
klass.where(attributes)
end
+
+ def where_object_changes_to(klass, attributes)
+ klass.where(attributes)
+ end
end
diff --git a/spec/support/paper_trail_spec_migrator.rb b/spec/support/paper_trail_spec_migrator.rb
index edd8f43..f53903c 100644
--- a/spec/support/paper_trail_spec_migrator.rb
+++ b/spec/support/paper_trail_spec_migrator.rb
@@ -18,22 +18,11 @@ class PaperTrailSpecMigrator
@migrations_path = dummy_app_migrations_dir
end
- # Looks like the API for programatically running migrations will change
- # in rails 5.2. This is an undocumented change, AFAICT. Then again,
- # how many people use the programmatic interface? Most people probably
- # just use rake. Maybe we're doing it wrong.
- #
- # See also discussion in https://github.com/rails/rails/pull/40806, when
- # MigrationContext#migrate became public.
def migrate
- if ::ActiveRecord.gem_version >= ::Gem::Version.new("6.0.0.rc2")
- ::ActiveRecord::MigrationContext.new(
- @migrations_path,
- ::ActiveRecord::Base.connection.schema_migration
- ).migrate
- else
- ::ActiveRecord::MigrationContext.new(@migrations_path).migrate
- end
+ ::ActiveRecord::MigrationContext.new(
+ @migrations_path,
+ ::ActiveRecord::Base.connection.schema_migration
+ ).migrate
end
# Generate a migration, run it, and delete it. We use this for testing the
diff --git a/spec/support/performance_helpers.rb b/spec/support/performance_helpers.rb
index 077a984..01c79b0 100644
--- a/spec/support/performance_helpers.rb
+++ b/spec/support/performance_helpers.rb
@@ -33,6 +33,6 @@ RSpec::Matchers.define :allocate_less_than do |expected|
failure_message do
"expected that example will allocate less than #{expected}#{@scale},"\
- " but allocated #{@allocated}#{@scale}"
+ " but allocated #{@allocated}#{@scale}"
end
end
diff --git a/spec/support/shared_examples/queries.rb b/spec/support/shared_examples/queries.rb
new file mode 100644
index 0000000..6264826
--- /dev/null
+++ b/spec/support/shared_examples/queries.rb
@@ -0,0 +1,388 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "queries" do |column_type, model, name_of_integer_column|
+ let(:record) { model.new }
+ let(:name) { FFaker::Name.first_name }
+ let(:int) { column_type == :text ? 1 : rand(2..6) }
+
+ after do
+ PaperTrail.serializer = PaperTrail::Serializers::YAML
+ end
+
+ describe "#where_attribute_changes", versioning: true do
+ it "requires its argument to be a string or a symbol" do
+ expect {
+ model.paper_trail.version_class.where_attribute_changes({})
+ }.to raise_error(ArgumentError)
+ expect {
+ model.paper_trail.version_class.where_attribute_changes([])
+ }.to raise_error(ArgumentError)
+ end
+
+ context "with object_changes_adapter configured" do
+ after do
+ PaperTrail.config.object_changes_adapter = nil
+ end
+
+ it "calls the adapter's where_attribute_changes method" do
+ adapter = instance_spy("CustomObjectChangesAdapter")
+ bicycle = model.create!(name: "abc")
+ bicycle.update!(name: "xyz")
+
+ allow(adapter).to(
+ receive(:where_attribute_changes).with(model.paper_trail.version_class, :name)
+ ).and_return([bicycle.versions[0], bicycle.versions[1]])
+
+ PaperTrail.config.object_changes_adapter = adapter
+ expect(
+ bicycle.versions.where_attribute_changes(:name)
+ ).to match_array([bicycle.versions[0], bicycle.versions[1]])
+ expect(adapter).to have_received(:where_attribute_changes)
+ end
+
+ it "defaults to the original behavior" do
+ adapter = Class.new.new
+ PaperTrail.config.object_changes_adapter = adapter
+ bicycle = model.create!(name: "abc")
+ bicycle.update!(name: "xyz")
+
+ if column_type == :text
+ expect {
+ bicycle.versions.where_attribute_changes(:name)
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_attribute_changes expected json or jsonb column, got text"
+ )
+ else
+ expect(
+ bicycle.versions.where_attribute_changes(:name)
+ ).to match_array([bicycle.versions[0], bicycle.versions[1]])
+ end
+ end
+ end
+
+ if column_type == :text
+ it "raises error" do
+ expect {
+ record.versions.where_attribute_changes(:name).to_a
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_attribute_changes expected json or jsonb column, got text"
+ )
+ end
+ else
+ it "locates versions according to their object_changes contents" do
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name_of_integer_column => 17)
+
+ expect(
+ record.versions.where_attribute_changes(:name)
+ ).to eq([record.versions[0]])
+ expect(
+ record.versions.where_attribute_changes(name_of_integer_column.to_s)
+ ).to eq([record.versions[0], record.versions[1]])
+ expect(record.class.column_names).to include("color")
+ expect(
+ record.versions.where_attribute_changes(:color)
+ ).to eq([])
+ end
+ end
+ end
+
+ describe "#where_object", versioning: true do
+ it "requires its argument to be a Hash" do
+ record.update!(name: name, name_of_integer_column => int)
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15)
+ expect {
+ model.paper_trail.version_class.where_object(:foo)
+ }.to raise_error(ArgumentError)
+ expect {
+ model.paper_trail.version_class.where_object([])
+ }.to raise_error(ArgumentError)
+ end
+
+ context "with YAML serializer" do
+ it "locates versions according to their `object` contents" do
+ expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML
+ record.update!(name: name, name_of_integer_column => int)
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15)
+ expect(
+ model.paper_trail.version_class.where_object(name_of_integer_column => int)
+ ).to eq([record.versions[1]])
+ expect(
+ model.paper_trail.version_class.where_object(name: name)
+ ).to eq([record.versions[1]])
+ expect(
+ model.paper_trail.version_class.where_object(name_of_integer_column => 100)
+ ).to eq([record.versions[2]])
+ end
+ end
+
+ context "with JSON serializer" do
+ it "locates versions according to their `object` contents" do
+ PaperTrail.serializer = PaperTrail::Serializers::JSON
+ expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON
+ record.update!(name: name, name_of_integer_column => int)
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15)
+ expect(
+ model.paper_trail.version_class.where_object(name_of_integer_column => int)
+ ).to eq([record.versions[1]])
+ expect(
+ model.paper_trail.version_class.where_object(name: name)
+ ).to eq([record.versions[1]])
+ expect(
+ model.paper_trail.version_class.where_object(name_of_integer_column => 100)
+ ).to eq([record.versions[2]])
+ end
+ end
+ end
+
+ describe "#where_object_changes", versioning: true do
+ it "requires its argument to be a Hash" do
+ expect {
+ model.paper_trail.version_class.where_object_changes(:foo)
+ }.to raise_error(ArgumentError)
+ expect {
+ model.paper_trail.version_class.where_object_changes([])
+ }.to raise_error(ArgumentError)
+ end
+
+ context "with object_changes_adapter configured" do
+ after do
+ PaperTrail.config.object_changes_adapter = nil
+ end
+
+ it "calls the adapter's where_object_changes method" do
+ adapter = instance_spy("CustomObjectChangesAdapter")
+ bicycle = model.create!(name: "abc")
+ allow(adapter).to(
+ receive(:where_object_changes).with(model.paper_trail.version_class, { name: "abc" })
+ ).and_return(bicycle.versions[0..1])
+ PaperTrail.config.object_changes_adapter = adapter
+ expect(
+ bicycle.versions.where_object_changes(name: "abc")
+ ).to match_array(bicycle.versions[0..1])
+ expect(adapter).to have_received(:where_object_changes)
+ end
+
+ it "defaults to the original behavior" do
+ adapter = Class.new.new
+ PaperTrail.config.object_changes_adapter = adapter
+ bicycle = model.create!(name: "abc")
+ if column_type == :text
+ expect {
+ bicycle.versions.where_object_changes(name: "abc")
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_object_changes expected json or jsonb column, got text"
+ )
+ else
+ expect(
+ bicycle.versions.where_object_changes(name: "abc")
+ ).to match_array(bicycle.versions[0..1])
+ end
+ end
+ end
+
+ if column_type == :text
+ it "raises error" do
+ expect {
+ record.versions.where_object_changes(name: "foo").to_a
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_object_changes expected json or jsonb column, got text"
+ )
+ end
+ else
+ it "locates versions according to their object_changes contents" do
+ record.update!(name: name, name_of_integer_column => 0)
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name: FFaker::Name.last_name, name_of_integer_column => int)
+ expect(
+ record.versions.where_object_changes(name: name)
+ ).to eq(record.versions[0..1])
+ expect(
+ record.versions.where_object_changes(name_of_integer_column => 100)
+ ).to eq(record.versions[1..2])
+ expect(
+ record.versions.where_object_changes(name_of_integer_column => int)
+ ).to eq([record.versions.last])
+ expect(
+ record.versions.where_object_changes(name_of_integer_column => 100, name: "foobar")
+ ).to eq(record.versions[1..2])
+ end
+ end
+ end
+
+ describe "#where_object_changes_from", versioning: true do
+ it "requires its argument to be a Hash" do
+ expect {
+ model.paper_trail.version_class.where_object_changes_from(:foo)
+ }.to raise_error(ArgumentError)
+ expect {
+ model.paper_trail.version_class.where_object_changes_from([])
+ }.to raise_error(ArgumentError)
+ end
+
+ context "with object_changes_adapter configured" do
+ after do
+ PaperTrail.config.object_changes_adapter = nil
+ end
+
+ it "calls the adapter's where_object_changes_from method" do
+ adapter = instance_spy("CustomObjectChangesAdapter")
+ bicycle = model.create!(name: "abc")
+ bicycle.update!(name: "xyz")
+
+ allow(adapter).to(
+ receive(:where_object_changes_from).with(model.paper_trail.version_class, { name: "abc" })
+ ).and_return([bicycle.versions[1]])
+
+ PaperTrail.config.object_changes_adapter = adapter
+ expect(
+ bicycle.versions.where_object_changes_from(name: "abc")
+ ).to match_array([bicycle.versions[1]])
+ expect(adapter).to have_received(:where_object_changes_from)
+ end
+
+ it "defaults to the original behavior" do
+ adapter = Class.new.new
+ PaperTrail.config.object_changes_adapter = adapter
+ bicycle = model.create!(name: "abc")
+ bicycle.update!(name: "xyz")
+
+ if column_type == :text
+ expect {
+ bicycle.versions.where_object_changes_from(name: "abc")
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_object_changes_from expected json or jsonb column, got text"
+ )
+ else
+ expect(
+ bicycle.versions.where_object_changes_from(name: "abc")
+ ).to match_array([bicycle.versions[1]])
+ end
+ end
+ end
+
+ if column_type == :text
+ it "raises error" do
+ expect {
+ record.versions.where_object_changes_from(name: "foo").to_a
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_object_changes_from expected json or jsonb column, got text"
+ )
+ end
+ else
+ it "locates versions according to their object_changes contents" do
+ record.update!(name: name, name_of_integer_column => 0)
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name: FFaker::Name.last_name, name_of_integer_column => int)
+
+ expect(
+ record.versions.where_object_changes_from(name: name)
+ ).to eq([record.versions[1]])
+ expect(
+ record.versions.where_object_changes_from(name_of_integer_column => 100)
+ ).to eq([record.versions[2]])
+ expect(
+ record.versions.where_object_changes_from(name_of_integer_column => int)
+ ).to eq([])
+ expect(
+ record.versions.where_object_changes_from(name_of_integer_column => 100, name: "foobar")
+ ).to eq([record.versions[2]])
+ end
+ end
+ end
+
+ describe "#where_object_changes_to", versioning: true do
+ it "requires its argument to be a Hash" do
+ expect {
+ model.paper_trail.version_class.where_object_changes_to(:foo)
+ }.to raise_error(ArgumentError)
+ expect {
+ model.paper_trail.version_class.where_object_changes_to([])
+ }.to raise_error(ArgumentError)
+ end
+
+ context "with object_changes_adapter configured" do
+ after do
+ PaperTrail.config.object_changes_adapter = nil
+ end
+
+ it "calls the adapter's where_object_changes_to method" do
+ adapter = instance_spy("CustomObjectChangesAdapter")
+ bicycle = model.create!(name: "abc")
+ bicycle.update!(name: "xyz")
+
+ allow(adapter).to(
+ receive(:where_object_changes_to).with(model.paper_trail.version_class, { name: "xyz" })
+ ).and_return([bicycle.versions[1]])
+
+ PaperTrail.config.object_changes_adapter = adapter
+ expect(
+ bicycle.versions.where_object_changes_to(name: "xyz")
+ ).to match_array([bicycle.versions[1]])
+ expect(adapter).to have_received(:where_object_changes_to)
+ end
+
+ it "defaults to the original behavior" do
+ adapter = Class.new.new
+ PaperTrail.config.object_changes_adapter = adapter
+ bicycle = model.create!(name: "abc")
+ bicycle.update!(name: "xyz")
+
+ if column_type == :text
+ expect {
+ bicycle.versions.where_object_changes_to(name: "xyz")
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_object_changes_to expected json or jsonb column, got text"
+ )
+ else
+ expect(
+ bicycle.versions.where_object_changes_to(name: "xyz")
+ ).to match_array([bicycle.versions[1]])
+ end
+ end
+ end
+
+ if column_type == :text
+ it "raises error" do
+ expect {
+ record.versions.where_object_changes_to(name: "foo").to_a
+ }.to raise_error(
+ ::PaperTrail::UnsupportedColumnType,
+ "where_object_changes_to expected json or jsonb column, got text"
+ )
+ end
+ else
+ it "locates versions according to their object_changes contents" do
+ record.update!(name: name, name_of_integer_column => 0)
+ record.update!(name: "foobar", name_of_integer_column => 100)
+ record.update!(name: FFaker::Name.last_name, name_of_integer_column => int)
+
+ expect(
+ record.versions.where_object_changes_to(name: name)
+ ).to eq([record.versions[0]])
+ expect(
+ record.versions.where_object_changes_to(name_of_integer_column => 100)
+ ).to eq([record.versions[1]])
+ expect(
+ record.versions.where_object_changes_to(name_of_integer_column => int)
+ ).to eq([record.versions[2]])
+ expect(
+ record.versions.where_object_changes_to(name_of_integer_column => 100, name: "foobar")
+ ).to eq([record.versions[1]])
+ expect(
+ record.versions.where_object_changes_to(name_of_integer_column => -1)
+ ).to eq([])
+ end
+ end
+ end
+end
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/lib/ruby/vendor_ruby/paper_trail/errors.rb -rw-r--r-- root/root /usr/lib/ruby/vendor_ruby/paper_trail/queries/versions/where_attribute_changes.rb -rw-r--r-- root/root /usr/lib/ruby/vendor_ruby/paper_trail/queries/versions/where_object_changes_to.rb -rw-r--r-- root/root /usr/share/rubygems-integration/all/specifications/paper_trail-14.0.0.gemspec
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/share/rubygems-integration/all/specifications/paper_trail-12.0.0.gemspec
No differences were encountered in the control files