Skip to content

Commit

Permalink
Merge pull request #277 from ActsAsParanoid/issue-232-fix-association…
Browse files Browse the repository at this point in the history
…-building

Fix association building for belongs_to with :with_deleted option
  • Loading branch information
mvz authored Apr 22, 2022
2 parents 4a7e2d6 + 88cc53b commit fdea727
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 20 deletions.
4 changes: 4 additions & 0 deletions lib/acts_as_paranoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require "acts_as_paranoid/associations"
require "acts_as_paranoid/validations"
require "acts_as_paranoid/relation"
require "acts_as_paranoid/association_reflection"

module ActsAsParanoid
def paranoid?
Expand Down Expand Up @@ -63,3 +64,6 @@ def acts_as_paranoid(options = {})

# Push the recover callback onto the activerecord callback list
ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover)

ActiveRecord::Reflection::AssociationReflection
.prepend ActsAsParanoid::AssociationReflection
41 changes: 41 additions & 0 deletions lib/acts_as_paranoid/association_reflection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module ActsAsParanoid
# Override for ActiveRecord::Reflection::AssociationReflection
#
# This makes automatic finding of inverse associations work where the
# inverse is a belongs_to association with the :with_deleted option set.
#
# Specifying :with_deleted for the belongs_to association would stop the
# inverse from being calculated because it sets scope where there was none,
# and normally an association having a scope means ActiveRecord will not
# automatically find the inverse association.
#
# This override adds an exception to that rule only for the case where the
# scope was added just to support the :with_deleted option.
module AssociationReflection
if ActiveRecord::VERSION::MAJOR < 7
def can_find_inverse_of_automatically?(reflection)
options = reflection.options

if reflection.macro == :belongs_to && options[:with_deleted]
return false if options[:inverse_of] == false
return false if options[:foreign_key]

!options.fetch(:original_scope)
else
super
end
end
else
def scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
if reflection.scope
options = reflection.options
return true if options[:with_deleted] && !options.fetch(:original_scope)
end

super
end
end
end
end
51 changes: 32 additions & 19 deletions lib/acts_as_paranoid/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,45 @@ def belongs_to_with_deleted(target, scope = nil, options = {})

with_deleted = options.delete(:with_deleted)
if with_deleted
if scope
old_scope = scope
scope = proc do |*args|
if old_scope.arity == 0
instance_exec(&old_scope).with_deleted
else
old_scope.call(*args).with_deleted
end
end
else
scope = proc do
if respond_to? :with_deleted
self.with_deleted
else
all
end
end
end
original_scope = scope
scope = make_scope_with_deleted(scope)
end

result = belongs_to_without_deleted(target, scope, **options)

result.values.last.options[:with_deleted] = with_deleted if with_deleted
if with_deleted
options = result.values.last.options
options[:with_deleted] = with_deleted
options[:original_scope] = original_scope
end

result
end

private

def make_scope_with_deleted(scope)
if scope
old_scope = scope
scope = proc do |*args|
if old_scope.arity == 0
instance_exec(&old_scope).with_deleted
else
old_scope.call(*args).with_deleted
end
end
else
scope = proc do
if respond_to? :with_deleted
with_deleted
else
all
end
end
end

scope
end
end
end
end
96 changes: 95 additions & 1 deletion test/test_associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ class ParanoidTime < ActiveRecord::Base
has_many :paranoid_has_many_dependants, dependent: :destroy
has_many :paranoid_booleans, dependent: :destroy
has_many :not_paranoids, dependent: :delete_all
# has_many :paranoid_sections, dependent: :destroy

has_one :has_one_not_paranoid, dependent: :destroy

Expand Down Expand Up @@ -138,6 +137,28 @@ class DoubleHasOneNotParanoid < HasOneNotParanoid
end
end

class ParanoidParent < ActiveRecord::Base
acts_as_paranoid
has_many :paranoid_children
has_many :paranoid_no_inverse_children
has_many :paranoid_foreign_key_children
end

class ParanoidChild < ActiveRecord::Base
acts_as_paranoid
belongs_to :paranoid_parent, with_deleted: true
end

class ParanoidNoInverseChild < ActiveRecord::Base
acts_as_paranoid
belongs_to :paranoid_parent, with_deleted: true, inverse_of: false
end

class ParanoidForeignKeyChild < ActiveRecord::Base
acts_as_paranoid
belongs_to :paranoid_parent, with_deleted: true, foreign_key: :paranoid_parent_id
end

# rubocop:disable Metrics/AbcSize
def setup
ActiveRecord::Schema.define(version: 1) do # rubocop:disable Metrics/BlockLength
Expand Down Expand Up @@ -252,6 +273,33 @@ def setup

timestamps t
end

create_table :paranoid_parents do |t|
t.datetime :deleted_at

timestamps t
end

create_table :paranoid_children do |t|
t.datetime :deleted_at
t.integer :paranoid_parent_id

timestamps t
end

create_table :paranoid_no_inverse_children do |t|
t.datetime :deleted_at
t.integer :paranoid_parent_id

timestamps t
end

create_table :paranoid_foreign_key_children do |t|
t.datetime :deleted_at
t.integer :paranoid_parent_id

timestamps t
end
end
end
# rubocop:enable Metrics/AbcSize
Expand Down Expand Up @@ -369,6 +417,52 @@ def test_belongs_to_with_deleted
assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time_with_deleted
end

def test_building_belongs_to_associations
paranoid_product = ParanoidProduct.new
paranoid_destroy_company =
ParanoidDestroyCompany.new(paranoid_products: [paranoid_product])

assert_equal paranoid_destroy_company,
paranoid_destroy_company.paranoid_products.first.paranoid_destroy_company
end

def test_building_belongs_to_associations_with_deleted
paranoid_child = ParanoidChild.new
paranoid_parent = ParanoidParent.new(paranoid_children: [paranoid_child])

assert_equal paranoid_parent, paranoid_parent.paranoid_children.first.paranoid_parent
end

def test_building_polymorphic_belongs_to_associations_with_deleted
paranoid_belongs_to = ParanoidBelongsToPolymorphic.new
paranoid_has_many =
ParanoidHasManyAsParent.new(paranoid_belongs_to_polymorphics: [paranoid_belongs_to])

assert_equal paranoid_has_many,
paranoid_has_many.paranoid_belongs_to_polymorphics.first.parent
end

def test_building_belongs_to_associations_with_deleted_with_inverse_of_false
paranoid_child = ParanoidNoInverseChild.new
paranoid_parent = ParanoidParent.new(paranoid_no_inverse_children: [paranoid_child])

assert_nil paranoid_parent.paranoid_no_inverse_children.first.paranoid_parent
end

def test_building_belongs_to_associations_with_deleted_with_foreign_key
paranoid_child = ParanoidForeignKeyChild.new
paranoid_parent = ParanoidParent.new(paranoid_foreign_key_children: [paranoid_child])

assert_nil paranoid_parent.paranoid_foreign_key_children.first.paranoid_parent
end

def test_belongs_to_with_deleted_as_inverse_of_has_many
has_many_reflection = ParanoidParent.reflect_on_association :paranoid_children
belongs_to_reflection = ParanoidChild.reflect_on_association :paranoid_parent

assert_equal belongs_to_reflection, has_many_reflection.inverse_of
end

def test_belongs_to_polymorphic_with_deleted
paranoid_time = ParanoidTime.create! name: "paranoid"
paranoid_has_many_dependant = ParanoidHasManyDependant
Expand Down

0 comments on commit fdea727

Please sign in to comment.