aliases/implications: clean up validations.

* Don't return true/false in validations (does nothing).
* Prefer `errors[:base]` over `self.errors[:base]`.
* Add antecedent_wiki / consequent_wiki associations.
* Factor out antecedent_and_consequent_are_different validation.
This commit is contained in:
evazion
2018-12-26 17:30:07 -06:00
parent ca7c56f95f
commit c27ba02b88
4 changed files with 51 additions and 56 deletions

View File

@@ -6,9 +6,8 @@ class TagAlias < TagRelationship
after_save :create_mod_action after_save :create_mod_action
validates_uniqueness_of :antecedent_name validates_uniqueness_of :antecedent_name
validate :absence_of_transitive_relation validate :absence_of_transitive_relation
validate :antecedent_and_consequent_are_different validate :consequent_has_wiki_page, on: :create, unless: :skip_secondary_validations
validate :consequent_has_wiki_page, :on => :create validate :mininum_antecedent_count, on: :create, unless: :skip_secondary_validations
validate :mininum_antecedent_count, :on => :create
module CacheMethods module CacheMethods
extend ActiveSupport::Concern extend ActiveSupport::Concern
@@ -101,17 +100,8 @@ class TagAlias < TagRelationship
def absence_of_transitive_relation def absence_of_transitive_relation
# We don't want a -> b && b -> c chains if the b -> c alias was created first. # We don't want a -> b && b -> c chains if the b -> c alias was created first.
# If the a -> b alias was created first, the new one will be allowed and the old one will be moved automatically instead. # If the a -> b alias was created first, the new one will be allowed and the old one will be moved automatically instead.
if self.class.active.exists?(["antecedent_name = ?", consequent_name]) if TagAlias.active.exists?(antecedent_name: consequent_name)
self.errors[:base] << "A tag alias for #{consequent_name} already exists" errors[:base] << "A tag alias for #{consequent_name} already exists"
false
end
end
def antecedent_and_consequent_are_different
normalize_names
if antecedent_name == consequent_name
self.errors[:base] << "Cannot alias a tag to itself"
false
end end
end end
@@ -164,8 +154,6 @@ class TagAlias < TagRelationship
if antecedent_tag.category != consequent_tag.category && antecedent_tag.category != Tag.categories.general if antecedent_tag.category != consequent_tag.category && antecedent_tag.category != Tag.categories.general
consequent_tag.update_attribute(:category, antecedent_tag.category) consequent_tag.update_attribute(:category, antecedent_tag.category)
end end
true
end end
def update_posts def update_posts
@@ -214,19 +202,14 @@ class TagAlias < TagRelationship
end end
def consequent_has_wiki_page def consequent_has_wiki_page
return if skip_secondary_validations if consequent_wiki.nil?
errors[:base] << "The #{consequent_name} tag needs a corresponding wiki page"
unless WikiPage.titled(consequent_name).exists?
self.errors[:base] << "The #{consequent_name} tag needs a corresponding wiki page"
return false
end end
end end
def mininum_antecedent_count def mininum_antecedent_count
return if skip_secondary_validations if antecedent_tag.post_count < 50
errors[:base] << "The #{antecedent_name} tag must have at least 50 posts for an alias to be created"
unless Post.fast_count(antecedent_name) >= 50
self.errors[:base] << "The #{antecedent_name} tag must have at least 50 posts for an alias to be created"
end end
end end

View File

@@ -12,8 +12,7 @@ class TagImplication < TagRelationship
validate :absence_of_transitive_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 :wiki_pages_present, on: :create, unless: :skip_secondary_validations
validate :wiki_pages_present, :on => :create
scope :old, ->{where("created_at between ? and ?", 2.months.ago, 1.month.ago)} scope :old, ->{where("created_at between ? and ?", 2.months.ago, 1.month.ago)}
scope :pending, ->{where(status: "pending")} scope :pending, ->{where(status: "pending")}
@@ -78,9 +77,8 @@ class TagImplication < TagRelationship
module ValidationMethods module ValidationMethods
def absence_of_circular_relation def absence_of_circular_relation
# We don't want a -> b && b -> a chains # We don't want a -> b && b -> a chains
if self.class.active.exists?(["antecedent_name = ? and consequent_name = ?", consequent_name, antecedent_name]) if TagImplication.active.exists?(["antecedent_name = ? and consequent_name = ?", consequent_name, antecedent_name])
self.errors[:base] << "Tag implication can not create a circular relation with another tag implication" errors[:base] << "Tag implication can not create a circular relation with another tag implication"
false
end end
end end
@@ -90,45 +88,31 @@ class TagImplication < TagRelationship
implications = TagImplication.active.where("antecedent_name = ? and consequent_name != ?", antecedent_name, consequent_name) implications = TagImplication.active.where("antecedent_name = ? and consequent_name != ?", antecedent_name, consequent_name)
implied_tags = implications.flat_map(&:descendant_names) implied_tags = implications.flat_map(&:descendant_names)
if implied_tags.include?(consequent_name) if implied_tags.include?(consequent_name)
self.errors[:base] << "#{antecedent_name} already implies #{consequent_name} through another implication" errors[:base] << "#{antecedent_name} already implies #{consequent_name} through another implication"
end end
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])
self.errors[:base] << "Antecedent tag must not be aliased to another tag" errors[:base] << "Antecedent tag must not be aliased to another tag"
false
end end
end end
def consequent_is_not_aliased def consequent_is_not_aliased
# We don't want to implicate a -> b if b is already aliased to c # We don't want to implicate a -> b if b is already aliased to c
if TagAlias.active.exists?(["antecedent_name = ?", consequent_name]) if TagAlias.active.exists?(["antecedent_name = ?", consequent_name])
self.errors[:base] << "Consequent tag must not be aliased to another tag" errors[:base] << "Consequent tag must not be aliased to another tag"
false
end
end
def antecedent_and_consequent_are_different
normalize_names
if antecedent_name == consequent_name
self.errors[:base] << "Cannot implicate a tag to itself"
false
end end
end end
def wiki_pages_present def wiki_pages_present
return if skip_secondary_validations if consequent_wiki.blank?
errors[:base] << "The #{consequent_name} tag needs a corresponding wiki page"
unless WikiPage.titled(consequent_name).exists?
self.errors[:base] << "The #{consequent_name} tag needs a corresponding wiki page"
return false
end end
unless WikiPage.titled(antecedent_name).exists? if antecedent_wiki.blank?
self.errors[:base] << "The #{antecedent_name} tag needs a corresponding wiki page" errors[:base] << "The #{antecedent_name} tag needs a corresponding wiki page"
return false
end end
end end
end end

View File

@@ -12,6 +12,8 @@ class TagRelationship < ApplicationRecord
belongs_to :forum_topic, optional: true belongs_to :forum_topic, optional: true
has_one :antecedent_tag, :class_name => "Tag", :foreign_key => "name", :primary_key => "antecedent_name" has_one :antecedent_tag, :class_name => "Tag", :foreign_key => "name", :primary_key => "antecedent_name"
has_one :consequent_tag, :class_name => "Tag", :foreign_key => "name", :primary_key => "consequent_name" has_one :consequent_tag, :class_name => "Tag", :foreign_key => "name", :primary_key => "consequent_name"
has_one :antecedent_wiki, through: :antecedent_tag, source: :wiki_page
has_one :consequent_wiki, through: :consequent_tag, source: :wiki_page
scope :active, ->{where(status: "active")} scope :active, ->{where(status: "active")}
scope :expired, ->{where("created_at < ?", EXPIRY.days.ago)} scope :expired, ->{where("created_at < ?", EXPIRY.days.ago)}
@@ -26,6 +28,7 @@ class TagRelationship < ApplicationRecord
validates :creator, presence: { message: "must exist" }, if: -> { creator_id.present? } validates :creator, presence: { message: "must exist" }, if: -> { creator_id.present? }
validates :approver, presence: { message: "must exist" }, if: -> { approver_id.present? } validates :approver, presence: { message: "must exist" }, if: -> { approver_id.present? }
validates :forum_topic, presence: { message: "must exist" }, if: -> { forum_topic_id.present? } validates :forum_topic, presence: { message: "must exist" }, if: -> { forum_topic_id.present? }
validate :antecedent_and_consequent_are_different
def initialize_creator def initialize_creator
self.creator_id = CurrentUser.user.id self.creator_id = CurrentUser.user.id
@@ -163,6 +166,12 @@ class TagRelationship < ApplicationRecord
end end
end end
def antecedent_and_consequent_are_different
if antecedent_name == consequent_name
errors[:base] << "Cannot alias or implicate a tag to itself"
end
end
extend SearchMethods extend SearchMethods
include MessageMethods include MessageMethods
end end

View File

@@ -42,6 +42,16 @@ class TagImplicationTest < ActiveSupport::TestCase
should_not allow_value(-1).for(:creator_id).with_message("must exist", against: :creator) should_not allow_value(-1).for(:creator_id).with_message("must exist", against: :creator)
end end
context "on secondary validation" do
should "warn if either tag is missing a wiki" do
ti = FactoryBot.build(:tag_implication, antecedent_name: "aaa", consequent_name: "bbb", skip_secondary_validations: false)
assert(ti.invalid?)
assert_includes(ti.errors[:base], "The aaa tag needs a corresponding wiki page")
assert_includes(ti.errors[:base], "The bbb tag needs a corresponding wiki page")
end
end
should "ignore pending implications when building descendant names" do should "ignore pending implications when building descendant names" do
ti2 = FactoryBot.build(:tag_implication, :antecedent_name => "b", :consequent_name => "c", :status => "pending") ti2 = FactoryBot.build(:tag_implication, :antecedent_name => "b", :consequent_name => "c", :status => "pending")
ti2.save ti2.save
@@ -54,6 +64,13 @@ class TagImplicationTest < ActiveSupport::TestCase
assert_equal(CurrentUser.user.id, ti.creator_id) assert_equal(CurrentUser.user.id, ti.creator_id)
end end
should "not validate when a tag directly implicates itself" do
ti = FactoryBot.build(:tag_implication, antecedent_name: "a", consequent_name: "a")
assert(ti.invalid?)
assert_includes(ti.errors[:base], "Cannot alias or implicate a tag to itself")
end
should "not validate when a circular relation is created" do should "not validate when a circular relation is created" do
ti1 = FactoryBot.create(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb") ti1 = FactoryBot.create(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb")
ti2 = FactoryBot.build(:tag_implication, :antecedent_name => "bbb", :consequent_name => "aaa") ti2 = FactoryBot.build(:tag_implication, :antecedent_name => "bbb", :consequent_name => "aaa")
@@ -79,12 +96,14 @@ class TagImplicationTest < ActiveSupport::TestCase
assert_includes(ti2.errors.full_messages, "Antecedent name has already been taken") 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 antecedent or consequent are aliased to another tag" do
ta = FactoryBot.create(:tag_alias, :antecedent_name => "bbb", :consequent_name => "ccc") ta1 = FactoryBot.create(:tag_alias, :antecedent_name => "aaa", :consequent_name => "a")
ta2 = FactoryBot.create(:tag_alias, :antecedent_name => "bbb", :consequent_name => "b")
ti = FactoryBot.build(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb") ti = FactoryBot.build(:tag_implication, :antecedent_name => "aaa", :consequent_name => "bbb")
ti.save
assert(ti.errors.any?, "Tag implication should not have validated.") assert(ti.invalid?)
assert_equal("Consequent tag must not be aliased to another tag", ti.errors.full_messages.join("")) assert_includes(ti.errors[:base], "Antecedent tag must not be aliased to another tag")
assert_includes(ti.errors[:base], "Consequent tag must not be aliased to another tag")
end end
should "calculate all its descendants" do should "calculate all its descendants" do