Add the ability to pass any options to the has_many :versions association macro

This commit is contained in:
Tyler Rick 2018-08-21 21:34:41 -07:00
parent 3f93befd88
commit 337e1a90f2
7 changed files with 136 additions and 17 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module PrefixVersionsInspectWithCount
def inspect
"#{length} versions:\n" +
super
end
end

View File

@ -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

19
spec/models/thing_spec.rb Normal file
View File

@ -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

View File

@ -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