Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make STI .joins() fix more consistent with existing tests #1

Open
wants to merge 12 commits into
base: sti-join-bug
Choose a base branch
from
2 changes: 0 additions & 2 deletions lib/paper_trail/events/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class Create < Base
# @api private
def data
data = {
item_id: @record.id,
item_type: @record.class.paper_trail.versions_association_item_type,
event: @record.paper_trail_event || "create",
whodunnit: PaperTrail.request.whodunnit
}
Expand Down
2 changes: 1 addition & 1 deletion lib/paper_trail/events/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Destroy < Base
def data
data = {
item_id: @record.id,
item_type: @record.class.paper_trail.versions_association_item_type,
item_type: @record.class.name,
event: @record.paper_trail_event || "destroy",
whodunnit: PaperTrail.request.whodunnit
}
Expand Down
2 changes: 0 additions & 2 deletions lib/paper_trail/events/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ def initialize(record, in_after_callback, is_touch, force_changes)
# @api private
def data
data = {
item_id: @record.id,
item_type: @record.class.paper_trail.versions_association_item_type,
event: @record.paper_trail_event || "update",
whodunnit: PaperTrail.request.whodunnit
}
Expand Down
33 changes: 18 additions & 15 deletions lib/paper_trail/model_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,6 @@ def version_class
@_version_class ||= @model_class.version_class_name.constantize
end

def versions_association_item_type
if @model_class.descends_from_active_record?
@model_class.base_class.name
else
@model_class.name
end
end

private

def active_record_gem_version
Expand Down Expand Up @@ -147,20 +139,31 @@ def cannot_record_after_destroy?
# is "Dog". If `attrs["species"]` is blank, `item_type` is "Animal". See
# `spec/models/animal_spec.rb`.
def setup_versions_association(klass)
klass.has_many(
has_manys = klass.has_many(
klass.versions_association_name,
lambda do
relation = order(model.timestamp_sort_order)
item_type = klass.paper_trail.send(:versions_association_item_type)
relation = relation.unscope(where: :item_type).where(item_type: item_type)
relation
order!(model.timestamp_sort_order)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order! is tagged :nodoc: meaning that it is private API. The rails team could remove it at any time without warning. I don't think we should use it.

unscope(where: :item_type).where(item_type: klass.name) unless klass.descendants.any?
end,
class_name: klass.version_class_name,
as: :item
)

# We override the assocation when STI models are created from a base class
# in order to use the right `item_type`.
# Only for this .versions association override HasManyAssociation#collection?
# (that normally always returns true) so it returns false when referring to
# a subclassed model that uses STI. Allows .create() events not to revert
# back to base_class at the final stages when Association#creation_attributes
# gets called.
has_manys[klass.versions_association_name.to_s].define_singleton_method(:collection?) do
active_record.descends_from_active_record?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How confident can we be that collection? will continue to be used by Rails in this way? Why does Rails have this method in the first place, just to return true?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How often does collection? end up getting called? I wonder if it's worth the effort to memoize the result of descends_from_active_record? so it's not calculated all the time.

BTW, when monkeypatching code I like to add a link to the version-specific lines of code like this: https://github.com/rails/rails/blob/v5.2.1/activerecord/lib/active_record/associations/association.rb#L198-L210

Since creation_attributes only sets the foreign key & type attributes, wouldn't it be more expressive to override it directly?

has_manys[klass.versions_association_name.to_s].singleton_class.prepend(Module.new {
  def creation_attributes
    if active_record.descends_from_active_record?
      super # sets `item_type` and `item_id`
    else
      {} # we set these elsewhere. Rails would incorrectly set them to the STI base class.
    end
  end
})

That raises the question: why don't we always pass in the correct item_type via this monkeypatch? If we're going to patch Rails, it might as well be consistent.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How confident can we be that collection? will continue to be used by Rails in this way?

It's been used in this way since Rails v4.0, and before that a very similar test was used, albeit one that wouldn't work out with our override:

if reflection.macro.in?([:has_one, :has_many]) && !options[:through]

Since creation_attributes only sets the foreign key & type attributes, wouldn't it be more expressive to override it directly?

At first I had hoped to have a solution that utilised this approach ... but it requires a little more code. The only way I see it working is as a class method added to the HasManyAssociation class, and inside a test to see if we're dealing with a PaperTrail::Version. For fun I've put that together in the latest commit. 10 lines of stuff as opposed to this little 4-liner.

I'm certainly OK with having this either way -- both approaches establish the same net result.

why don't we always pass in the correct item_type via this monkeypatch?

It's very specific just to PaperTrail::Version objects, and only has effect for anything subclassed.

Copy link
Owner

@seanlinsley seanlinsley Aug 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: paper-trail-gem#1137 (comment)

Are we okay with globally patching creation_attributes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points on creation_attributes -- have gone back to directly patching collection? on the specific :verisons HasManyReflection objects that are actually subclassed. Much less intrusive.

end

setup_versions_association_when_inheriting(klass)
end

# When STI models are created from a base class, override the otherwise-inherited
# has_many :versions in order to use the right `item_type`.
def setup_versions_association_when_inheriting(klass)
klass.singleton_class.prepend(Module.new {
def inherited(klass)
super
Expand Down
6 changes: 3 additions & 3 deletions lib/paper_trail/record_trail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def record_create
# `data_for_create` but PT-AT still does.
data = event.data.merge(data_for_create)

version = @record.class.paper_trail.version_class.new(data)
version = @record.send(@record.class.versions_association_name).new(data)
version.save!
version
end
Expand Down Expand Up @@ -138,7 +138,7 @@ def record_update(force:, in_after_callback:, is_touch:)
# `data_for_update` but PT-AT still does.
data = event.data.merge(data_for_update)

version = @record.class.paper_trail.version_class.new(data)
version = @record.send(@record.class.versions_association_name).new(data)
if version.save
version
else
Expand All @@ -164,7 +164,7 @@ def record_update_columns(changes)
# `data_for_update_columns` but PT-AT still does.
data = event.data.merge(data_for_update_columns)

version = @record.class.paper_trail.version_class.new(data)
version = @record.send(@record.class.versions_association_name).new(data)
if version.save
version
else
Expand Down
4 changes: 2 additions & 2 deletions spec/controllers/articles_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
post :create, params_wrapper(article: { title: "Doh", content: FFaker::Lorem.sentence })
expect(assigns(:article)).not_to be_nil
expect(PaperTrail.request.enabled?).to eq(true)
expect(assigns(:article).versions.count).to eq(1)
expect(assigns(:article).versions.length).to eq(1)
end

after { PaperTrail.enabled = false }
Expand All @@ -23,7 +23,7 @@
expect(PaperTrail.enabled?).to eq(false)
post :create, params_wrapper(article: { title: "Doh", content: FFaker::Lorem.sentence })
expect(PaperTrail.request.enabled?).to eq(false)
expect(assigns(:article).versions.count).to eq(0)
expect(assigns(:article).versions.length).to eq(0)
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/models/json_version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
context "valid arguments", versioning: true do
it "locates versions according to their `object` contents" do
fruit = Fruit.create!(name: "apple")
expect(fruit.versions.count).to eq(1)
expect(fruit.versions.length).to eq(1)
fruit.update_attributes!(name: "banana", color: "aqua")
expect(fruit.versions.count).to eq(2)
expect(fruit.versions.length).to eq(2)
fruit.update_attributes!(name: "coconut", color: "black")
expect(fruit.versions.count).to eq(3)
expect(fruit.versions.length).to eq(3)
where_apple = described_class.where_object(name: "apple")
expect(where_apple.to_sql).to eq(
<<-SQL.squish
Expand Down
4 changes: 2 additions & 2 deletions spec/models/on/empty_array_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ module On
describe ".paper_trail.update_columns" do
it "creates a version record" do
widget = Widget.create
assert_equal 1, widget.versions.count
assert_equal 1, widget.versions.length
widget.paper_trail.update_columns(name: "Bugle")
assert_equal 2, widget.versions.count
assert_equal 2, widget.versions.length
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/models/pet_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@

# After creating a bunch of records above, we change the inheritance_column
# so that we can demonstrate passing hints to the migration generator.
xcontext "simulate a historical change to inheritance_column" do
context "simulate a historical change to inheritance_column" do
before do
Animal.inheritance_column = "species_xyz"
end
Expand Down
4 changes: 2 additions & 2 deletions spec/models/widget_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@
describe "#update", versioning: true do
it "creates a version record" do
widget = Widget.create
assert_equal 1, widget.versions.count
assert_equal 1, widget.versions.length
widget.update_attributes(name: "Bugle")
assert_equal 2, widget.versions.count
assert_equal 2, widget.versions.length
end
end
end
6 changes: 3 additions & 3 deletions spec/paper_trail/cleaner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ module PaperTrail
date = animal.versions.first.created_at.to_date
animal.update_attribute(:name, FFaker::Name.name)
expect(PaperTrail::Version.count).to(eq(10))
expect(animal.versions.count).to(eq(4))
expect(animal.versions.size).to(eq(4))
expect(animal.paper_trail.versions_between(date, (date + 1.day)).size).to(eq(3))
PaperTrail.clean_versions!(date: date)
expect(PaperTrail::Version.count).to(eq(8))
expect(animal.versions.count).to(eq(2))
expect(animal.versions.reload.size).to(eq(2))
expect(animal.versions.first.created_at.to_date).to(eq(date))
# Why use `equal?` here instead of something less strict?
# Doesn't `to_date` always produce a new date object?
Expand Down Expand Up @@ -100,7 +100,7 @@ module PaperTrail
date = animal.versions.first.created_at.to_date
expect(PaperTrail::Version.count).to(eq(11))
[animal, dog].each do |animal|
expect(animal.versions.count).to(eq(4))
expect(animal.versions.size).to(eq(4))
expect(animal.versions.between(date, (date + 1.day)).size).to(eq(3))
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/paper_trail/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
before { @widget.update_attributes(name: "Ford") }

it "add to its trail" do
expect(@widget.versions.count).to(eq((@count + 1)))
expect(@widget.versions.length).to(eq((@count + 1)))
end
end
end
Expand Down