Merge pull request #3201 from evazion/fix-transitive-implications

Fix #3200: Disallow creation of superfluous implications
This commit is contained in:
Albert Yi
2017-07-05 11:50:51 -07:00
committed by GitHub
3 changed files with 53 additions and 4 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
@@ -31,7 +32,7 @@ class TagImplication < ApplicationRecord
module ClassMethods module ClassMethods
# assumes names are normalized # assumes names are normalized
def with_descendants(names) def with_descendants(names)
(names + where("antecedent_name in (?) and status in (?)", names, ["active", "processing"]).map(&:descendant_names_array)).flatten.uniq (names + active.where(antecedent_name: names).flat_map(&:descendant_names_array)).uniq
end end
def automatic_tags_for(names) def automatic_tags_for(names)
@@ -48,7 +49,7 @@ class TagImplication < ApplicationRecord
until children.empty? until children.empty?
all.concat(children) all.concat(children)
children = TagImplication.where("antecedent_name IN (?) and status in (?)", children, ["active", "processing"]).map(&:consequent_name) children = TagImplication.active.where(antecedent_name: children).pluck(:consequent_name)
end end
end.sort.uniq end.sort.uniq
end end
@@ -96,7 +97,7 @@ class TagImplication < ApplicationRecord
end end
def active def active
where("status IN (?)", ["active", "processing"]) where(status: %w[active processing queued])
end end
def search(params) def search(params)
@@ -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