Fix #3200: Disallow creation of superfluous implications.

Disallow transitive implications. If a -> b -> c already exists, don't
allow a -> c.

Caveat: if b -> c already exists, and we make a BUR for a -> b and a -> c,
the BUR validates even though a -> c is redundant. It only fails
when the BUR is approved.
This commit is contained in:
evazion
2017-07-04 17:02:17 -05:00
parent cda35494a4
commit 542c673221
3 changed files with 50 additions and 1 deletions

View File

@@ -18,6 +18,7 @@ class TagImplication < ApplicationRecord
validates :forum_topic, presence: { message: "must exist" }, if: lambda { forum_topic_id.present? } validates :forum_topic, presence: { message: "must exist" }, if: lambda { forum_topic_id.present? }
validates_uniqueness_of :antecedent_name, :scope => :consequent_name validates_uniqueness_of :antecedent_name, :scope => :consequent_name
validate :absence_of_circular_relation validate :absence_of_circular_relation
validate :absence_of_transitive_relation
validate :antecedent_is_not_aliased validate :antecedent_is_not_aliased
validate :consequent_is_not_aliased validate :consequent_is_not_aliased
validate :antecedent_and_consequent_are_different validate :antecedent_and_consequent_are_different
@@ -137,6 +138,16 @@ class TagImplication < ApplicationRecord
end end
end end
# If we already have a -> b -> c, don't allow a -> c.
def absence_of_transitive_relation
# Find everything else the antecedent implies, not including the current implication.
implications = TagImplication.active.where("antecedent_name = ? and consequent_name != ?", antecedent_name, consequent_name)
implied_tags = implications.flat_map(&:descendant_names_array)
if implied_tags.include?(consequent_name)
self.errors[:base] << "#{antecedent_name} already implies #{consequent_name} through another implication"
end
end
def antecedent_is_not_aliased def antecedent_is_not_aliased
# We don't want to implicate a -> b if a is already aliased to c # We don't want to implicate a -> b if a is already aliased to c
if TagAlias.active.exists?(["antecedent_name = ?", antecedent_name]) if TagAlias.active.exists?(["antecedent_name = ?", antecedent_name])

View File

@@ -62,6 +62,35 @@ class BulkUpdateRequestTest < ActiveSupport::TestCase
end end
end end
context "for an implication that is redundant with an existing implication" do
should "not validate" do
FactoryGirl.create(:tag_implication, :antecedent_name => "a", :consequent_name => "b")
FactoryGirl.create(:tag_implication, :antecedent_name => "b", :consequent_name => "c")
bur = FactoryGirl.build(:bulk_update_request, :script => "imply a -> c")
bur.save
assert_equal(["Error: a already implies c through another implication (create implication a -> c)"], bur.errors.full_messages)
end
end
context "for an implication that is redundant with another implication in the same BUR" do
setup do
FactoryGirl.create(:tag_implication, :antecedent_name => "b", :consequent_name => "c")
@bur = FactoryGirl.build(:bulk_update_request, :script => "imply a -> b\nimply a -> c")
@bur.save
end
should "not process" do
assert_no_difference("TagImplication.count") do
@bur.approve!(@admin)
end
end
should_eventually "not validate" do
assert_equal(["Error: a already implies c through another implication (create implication a -> c)"], @bur.errors.full_messages)
end
end
context "with an associated forum topic" do context "with an associated forum topic" do
setup do setup do
@topic = FactoryGirl.create(:forum_topic, :title => "[bulk] hoge") @topic = FactoryGirl.create(:forum_topic, :title => "[bulk] hoge")

View File

@@ -66,12 +66,21 @@ class TagImplicationTest < ActiveSupport::TestCase
assert_equal("Tag implication can not create a circular relation with another tag implication", ti2.errors.full_messages.join("")) assert_equal("Tag implication can not create a circular relation with another tag implication", ti2.errors.full_messages.join(""))
end end
should "not validate when a transitive relation is created" do
ti_ab = FactoryGirl.create(:tag_implication, :antecedent_name => "a", :consequent_name => "b")
ti_bc = FactoryGirl.create(:tag_implication, :antecedent_name => "b", :consequent_name => "c")
ti_ac = FactoryGirl.build(:tag_implication, :antecedent_name => "a", :consequent_name => "c")
ti_ac.save
assert_equal("a already implies c through another implication", ti_ac.errors.full_messages.join(""))
end
should "not allow for duplicates" do should "not allow for duplicates" do
ti1 = FactoryGirl.create(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb") ti1 = FactoryGirl.create(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb")
ti2 = FactoryGirl.build(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb") ti2 = FactoryGirl.build(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb")
ti2.save ti2.save
assert(ti2.errors.any?, "Tag implication should not have validated.") assert(ti2.errors.any?, "Tag implication should not have validated.")
assert_equal("Antecedent name has already been taken", ti2.errors.full_messages.join("")) assert_includes(ti2.errors.full_messages, "Antecedent name has already been taken")
end end
should "not validate if its consequent is aliased to another tag" do should "not validate if its consequent is aliased to another tag" do