From baba6efd04db0a428b5c54e1e75905b9b7f7c07f Mon Sep 17 00:00:00 2001 From: Enrico Brunetta Date: Fri, 22 Mar 2019 12:10:56 -0700 Subject: [PATCH] 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? --- CHANGELOG.md | 4 +- README.md | 24 ++++++++++++ lib/paper_trail/model_config.rb | 16 ++++++++ lib/paper_trail/reifier.rb | 1 + lib/paper_trail/version_concern.rb | 22 ++++++++++- spec/dummy_app/app/models/limited_bicycle.rb | 5 +++ .../dummy_app/app/models/unlimited_bicycle.rb | 5 +++ spec/paper_trail/config_spec.rb | 39 ++++++++++++++++++- spec/paper_trail/version_limit_spec.rb | 13 +++++++ 9 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 spec/dummy_app/app/models/limited_bicycle.rb create mode 100644 spec/dummy_app/app/models/unlimited_bicycle.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b8dfcbb..4c9393935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 7c37beeb9..e9eb7c4e5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/paper_trail/model_config.rb b/lib/paper_trail/model_config.rb index d92a8cff4..96e42afe4 100644 --- a/lib/paper_trail/model_config.rb +++ b/lib/paper_trail/model_config.rb @@ -18,6 +18,11 @@ class ModelConfig `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 @@ def setup(options = {}) @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 @@ def cannot_record_after_destroy? ::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 diff --git a/lib/paper_trail/reifier.rb b/lib/paper_trail/reifier.rb index d2a60bce4..44ce5095d 100644 --- a/lib/paper_trail/reifier.rb +++ b/lib/paper_trail/reifier.rb @@ -120,6 +120,7 @@ def reify_attributes(model, version, attrs) # 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] diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb index ff93e47ef..69db2fd16 100644 --- a/lib/paper_trail/version_concern.rb +++ b/lib/paper_trail/version_concern.rb @@ -25,6 +25,10 @@ module VersionConcern # :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 @@ def object_changes_deserialized # 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 @@ def enforce_version_limit! 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 diff --git a/spec/dummy_app/app/models/limited_bicycle.rb b/spec/dummy_app/app/models/limited_bicycle.rb new file mode 100644 index 000000000..5d93434fd --- /dev/null +++ b/spec/dummy_app/app/models/limited_bicycle.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class LimitedBicycle < Vehicle + has_paper_trail limit: 3 +end diff --git a/spec/dummy_app/app/models/unlimited_bicycle.rb b/spec/dummy_app/app/models/unlimited_bicycle.rb new file mode 100644 index 000000000..8b493651d --- /dev/null +++ b/spec/dummy_app/app/models/unlimited_bicycle.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class UnlimitedBicycle < Vehicle + has_paper_trail limit: nil +end diff --git a/spec/paper_trail/config_spec.rb b/spec/paper_trail/config_spec.rb index bcaa63c0e..f21d48cde 100644 --- a/spec/paper_trail/config_spec.rb +++ b/spec/paper_trail/config_spec.rb @@ -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 diff --git a/spec/paper_trail/version_limit_spec.rb b/spec/paper_trail/version_limit_spec.rb index 1dc6769b0..f981b8afe 100644 --- a/spec/paper_trail/version_limit_spec.rb +++ b/spec/paper_trail/version_limit_spec.rb @@ -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