add 'limit' option to has_paper_trail to override global PaperTrail.config.version_limit setting (#1194)

* add 'limit' option to has_paper_trail allowing users to override the PaperTrail.config.version_limit value on a per-model basis. This feature requires the item_subtype column in the versions table.

* Suggestions to be squashed into PR 1194

* Squash: trim trailing whitespace

[ci skip]

* Squash: use item_subtype_column_present?
This commit is contained in:
Enrico Brunetta 2019-03-22 12:10:56 -07:00 committed by Jared Beck
parent 970b70ecbf
commit 8b6876ace0
9 changed files with 126 additions and 3 deletions

View File

@ -11,7 +11,9 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
### Added
- None
- [#1194](https://github.com/paper-trail-gem/paper_trail/pull/1194) -
Added a 'limit' option to has_paper_trail, allowing models to override the
global `PaperTrail.config.version_limit` setting.
### Fixed

View File

@ -580,6 +580,30 @@ PaperTrail.config.version_limit = 3
PaperTrail.config.version_limit = nil
```
#### 2.e.1 Per-model limit
Models can override the global `PaperTrail.config.version_limit` setting.
Example:
```
# initializer
PaperTrail.config.version_limit = 10
# At most 10 versions
has_paper_trail
# At most 3 versions (2 updates, 1 create). Overrides global version_limit.
has_paper_trail limit: 2
# Infinite versions
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

View File

@ -18,6 +18,11 @@ 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.
@ -112,6 +117,7 @@ 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
@ -139,6 +145,16 @@ module PaperTrail
::ActiveRecord::Base.belongs_to_required_by_default
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)
end
def check_version_class_name(options)
# @api private - `version_class_name`
@model_class.class_attribute :version_class_name

View File

@ -120,6 +120,7 @@ module PaperTrail
# this method returns the constant `Animal`. You can see this particular
# example in action in `spec/models/animal_spec.rb`.
#
# TODO: Duplication: similar `constantize` in VersionConcern#version_limit
def version_reification_class(version, attrs)
inheritance_column_name = version.item_type.constantize.inheritance_column
inher_col_value = attrs[inheritance_column_name]

View File

@ -25,6 +25,10 @@ module PaperTrail
# :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
@ -329,7 +333,7 @@ module PaperTrail
# Enforces the `version_limit`, if set. Default: no limit.
# @api private
def enforce_version_limit!
limit = PaperTrail.config.version_limit
limit = version_limit
return unless limit.is_a? Numeric
previous_versions = sibling_versions.not_creates.
order(self.class.timestamp_sort_order("asc"))
@ -337,5 +341,21 @@ module PaperTrail
excess_versions = previous_versions - previous_versions.last(limit)
excess_versions.map(&:destroy)
end
# See docs section 2.e. Limiting the Number of Versions Created.
# 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
end
PaperTrail.config.version_limit
end
end
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class LimitedBicycle < Vehicle
has_paper_trail limit: 3
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class UnlimitedBicycle < Vehicle
has_paper_trail limit: nil
end

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
require "securerandom"
require "spec_helper"
module PaperTrail
@ -38,10 +39,46 @@ module PaperTrail
it "limits the number of versions to 3 (2 plus the created at event)" do
PaperTrail.config.version_limit = 2
widget = Widget.create!(name: "Henry")
6.times { widget.update_attribute(:name, FFaker::Lorem.word) }
6.times { widget.update_attribute(:name, SecureRandom.hex(8)) }
expect(widget.versions.first.event).to(eq("create"))
expect(widget.versions.size).to(eq(3))
end
it "overrides the general limits to 4 (3 plus the created at event)" do
PaperTrail.config.version_limit = 100
bike = LimitedBicycle.create!(name: "Limited Bike") # has_paper_trail limit: 3
10.times { bike.update_attribute(:name, SecureRandom.hex(8)) }
expect(bike.versions.first.event).to(eq("create"))
expect(bike.versions.size).to(eq(4))
end
it "overrides the general limits with unlimited versions for model" do
PaperTrail.config.version_limit = 3
bike = UnlimitedBicycle.create!(name: "Unlimited Bike") # has_paper_trail limit: nil
6.times { bike.update_attribute(:name, SecureRandom.hex(8)) }
expect(bike.versions.first.event).to(eq("create"))
expect(bike.versions.size).to eq(7)
end
it "is not enabled on non-papertrail STI base classes, but enabled on subclasses" do
PaperTrail.config.version_limit = 10
Vehicle.create!(name: "A Vehicle", type: "Vehicle")
limited_bike = LimitedBicycle.create!(name: "Limited")
limited_bike.name = "A new name"
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

View File

@ -8,6 +8,19 @@ module PaperTrail
PaperTrail.config.version_limit = nil
end
it "cleans up old versions with limit specified in model" do
PaperTrail.config.version_limit = 10
# LimitedBicycle overrides the global version_limit
bike = LimitedBicycle.create(name: "Bike") # has_paper_trail limit: 3
15.times do |i|
bike.update(name: "Name #{i}")
end
expect(LimitedBicycle.find(bike.id).versions.count).to eq(4)
# 4 versions = 3 updates + 1 create.
end
it "cleans up old versions" do
PaperTrail.config.version_limit = 10
widget = Widget.create