diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf6dd56..a7910d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/). ### Added -- None +- [#1158](https://github.com/paper-trail-gem/paper_trail/pull/1158) — Add the ability to pass + options, such as `scope` or `extend:` to the `has_many :versions` association macro. ### Fixed diff --git a/README.md b/README.md index cd8d7ab6..b684945b 100644 --- a/README.md +++ b/README.md @@ -955,7 +955,7 @@ see https://github.com/paper-trail-gem/paper_trail/issues/594 ### 5.b. Configuring the `versions` Association You may configure the name of the `versions` association by passing -a different name to `has_paper_trail`. +a different name to `has_paper_trail`: ```ruby class Post < ActiveRecord::Base @@ -968,6 +968,23 @@ Post.new.versions # => NoMethodError Overriding (instead of configuring) the `versions` method is not supported. Overriding associations is not recommended in general. +You may pass other options for the `has_many` by passing a hash of options: + +```ruby +class Post < ActiveRecord::Base + has_paper_trail versions: { + name: :drafts, + scope: -> { order("id desc") }, + extend: VersionsExtensions + } +end +``` + +Refer to +https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many-label-Options +for the full list of supported options for `has_many`. + + ### 5.c. Generators PaperTrail has one generator, `paper_trail:install`. It writes, but does not diff --git a/lib/paper_trail/model_config.rb b/lib/paper_trail/model_config.rb index 3303b270..ca66d44c 100644 --- a/lib/paper_trail/model_config.rb +++ b/lib/paper_trail/model_config.rb @@ -142,21 +142,35 @@ module PaperTrail @model_class.class_attribute :version_class_name @model_class.version_class_name = options[:class_name] || "PaperTrail::Version" - # @api private - versions_association_name - @model_class.class_attribute :versions_association_name - @model_class.versions_association_name = options[:versions] || :versions + assert_concrete_activerecord_class(@model_class.version_class_name) # @api public - paper_trail_event @model_class.send :attr_accessor, :paper_trail_event - assert_concrete_activerecord_class(@model_class.version_class_name) + setup_has_many_versions(options) + end + + def setup_has_many_versions(options) + unless options[:versions].is_a?(Hash) + options[:versions] = { + name: options[:versions] + } + end + + # @api private - versions_association_name + @model_class.class_attribute :versions_association_name + @model_class.versions_association_name = options[:versions].delete(:name) || :versions + + scope = options[:versions].delete(:scope) || -> { order(model.timestamp_sort_order) } + options[:versions].assert_valid_keys(valid_keys_for_has_many) # @api public @model_class.has_many( @model_class.versions_association_name, - -> { order(model.timestamp_sort_order) }, + scope, class_name: @model_class.version_class_name, - as: :item + as: :item, + **options[:versions] ) end @@ -182,5 +196,19 @@ module PaperTrail @model_class.paper_trail_options[:meta] ||= {} end + + # @api private + def valid_keys_for_has_many + if ActiveRecord.gem_version >= Gem::Version.new("5.0") + ActiveRecord::Associations::Builder::HasMany.valid_options({}) + else + %i[ + after_add after_remove anonymous_class as autosave before_add + before_remove class_name counter_cache dependent extend foreign_key + foreign_type inverse_of join_table primary_key source source_type + table_name through validate + ] + end + end end end diff --git a/spec/dummy_app/app/models/concerns/prefix_versions_inspect_with_count.rb b/spec/dummy_app/app/models/concerns/prefix_versions_inspect_with_count.rb new file mode 100644 index 00000000..36c6170b --- /dev/null +++ b/spec/dummy_app/app/models/concerns/prefix_versions_inspect_with_count.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module PrefixVersionsInspectWithCount + def inspect + "#{length} versions:\n" + + super + end +end diff --git a/spec/dummy_app/app/models/thing.rb b/spec/dummy_app/app/models/thing.rb index 2d1a8dd9..504e96a2 100644 --- a/spec/dummy_app/app/models/thing.rb +++ b/spec/dummy_app/app/models/thing.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true class Thing < ActiveRecord::Base - has_paper_trail + has_paper_trail versions: { + extend: PrefixVersionsInspectWithCount, + scope: -> { order("id desc") } + } if ActiveRecord.gem_version >= Gem::Version.new("5.0") belongs_to :person, optional: true diff --git a/spec/models/thing_spec.rb b/spec/models/thing_spec.rb new file mode 100644 index 00000000..77c69c33 --- /dev/null +++ b/spec/models/thing_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Thing, type: :model do + describe "#versions", versioning: true do + let(:thing) { Thing.create! } + + it "applies the extend option" do + expect(thing.versions.singleton_class).to be < PrefixVersionsInspectWithCount + expect(thing.versions.inspect).to start_with("1 versions:") + end + + it "applies the scope option" do + expect(Thing.reflect_on_association(:versions).scope).to be_a Proc + expect(thing.versions.to_sql).to end_with "ORDER BY id desc" + end + end +end diff --git a/spec/paper_trail/model_config_spec.rb b/spec/paper_trail/model_config_spec.rb index d512e0f4..054c18b5 100644 --- a/spec/paper_trail/model_config_spec.rb +++ b/spec/paper_trail/model_config_spec.rb @@ -4,15 +4,58 @@ require "spec_helper" module PaperTrail ::RSpec.describe ModelConfig do - describe "when has_paper_trail is called" do - it "raises an error" do - expect { - class MisconfiguredCVC < ActiveRecord::Base - has_paper_trail class_name: "AbstractVersion" + describe "has_paper_trail" do + describe "passing an abstract class to class_name" do + it "raises an error" do + expect { + Class.new(ActiveRecord::Base) do + has_paper_trail class_name: "AbstractVersion" + end + }.to raise_error( + /use concrete \(not abstract\) version models/ + ) + end + end + + describe "versions:" do + it "name can be passed instead of an options hash" do + klass = Class.new(ActiveRecord::Base) do + has_paper_trail versions: :drafts end - }.to raise_error( - /use concrete \(not abstract\) version models/ - ) + expect(klass.reflect_on_association(:drafts)).to be_a( + ActiveRecord::Reflection::HasManyReflection + ) + end + it "name can be passed in the options hash" do + klass = Class.new(ActiveRecord::Base) do + has_paper_trail versions: { name: :drafts } + end + expect(klass.reflect_on_association(:drafts)).to be_a( + ActiveRecord::Reflection::HasManyReflection + ) + end + it "allows any option that has_many supports" do + klass = Class.new(ActiveRecord::Base) do + has_paper_trail versions: { autosave: true, validate: true } + end + expect(klass.reflect_on_association(:versions).options[:autosave]).to eq true + expect(klass.reflect_on_association(:versions).options[:validate]).to eq true + end + it "can even override options that PaperTrail adds to has_many" do + klass = Class.new(ActiveRecord::Base) do + has_paper_trail versions: { as: :foo } + end + expect(klass.reflect_on_association(:versions).options[:as]).to eq :foo + end + it "raises an error on unknown has_many options" do + expect { + Class.new(ActiveRecord::Base) do + has_paper_trail versions: { read_my_mind: true, validate: true } + end + }.to raise_error( + /Unknown key: :read_my_mind. Valid keys are: .*:class_name,/ + ) + end end end end