implications: refactor calculation of implied tags.
Refactor to use a recursive CTE to calculate implied tags in SQL, rather than storing them in a descendant_names field. This avoids the complexity of keeping the stored field up to date. It's also more flexible, since it allows us to find both descendant tags (tags that imply a given tag) as well as ancestor tags (tags that are implied by a given tag).
This commit is contained in:
@@ -643,7 +643,7 @@ class Post < ApplicationRecord
|
||||
normalized_tags = remove_invalid_tags(normalized_tags)
|
||||
normalized_tags = Tag.convert_cosplay_tags(normalized_tags)
|
||||
normalized_tags += Tag.create_for_list(TagImplication.automatic_tags_for(normalized_tags))
|
||||
normalized_tags = TagImplication.with_descendants(normalized_tags)
|
||||
normalized_tags += TagImplication.tags_implied_by(normalized_tags).map(&:name)
|
||||
normalized_tags = normalized_tags.compact.uniq.sort
|
||||
normalized_tags = Tag.create_for_list(normalized_tags)
|
||||
set_tag_string(normalized_tags.join(" "))
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
class TagImplication < TagRelationship
|
||||
array_attribute :descendant_names
|
||||
|
||||
has_many :child_implications, class_name: "TagImplication", primary_key: :consequent_name, foreign_key: :antecedent_name
|
||||
has_many :parent_implications, class_name: "TagImplication", primary_key: :antecedent_name, foreign_key: :consequent_name
|
||||
|
||||
before_save :update_descendant_names
|
||||
after_save :update_descendant_names_for_parents
|
||||
after_destroy :update_descendant_names_for_parents
|
||||
after_save :create_mod_action
|
||||
validates_uniqueness_of :antecedent_name, scope: [:consequent_name, :status], conditions: -> { active }
|
||||
validate :absence_of_circular_relation
|
||||
@@ -19,11 +14,6 @@ class TagImplication < TagRelationship
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
# assumes names are normalized
|
||||
def with_descendants(names)
|
||||
(names + active.where(antecedent_name: names).flat_map(&:descendant_names)).uniq
|
||||
end
|
||||
|
||||
def automatic_tags_for(names)
|
||||
tags = []
|
||||
tags += names.grep(/\A(.+)_\(cosplay\)\z/i) { "char:#{TagAlias.to_aliased([$1]).first}" }
|
||||
@@ -32,31 +22,28 @@ class TagImplication < TagRelationship
|
||||
tags.uniq
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def descendants
|
||||
[].tap do |all|
|
||||
children = [consequent_name]
|
||||
|
||||
until children.empty?
|
||||
all.concat(children)
|
||||
children = TagImplication.active.where(antecedent_name: children).pluck(:consequent_name)
|
||||
concerning :HierarchyMethods do
|
||||
class_methods do
|
||||
def ancestors_of(names)
|
||||
join_recursive do |query|
|
||||
query.start_with(antecedent_name: names).connect_by(consequent_name: :antecedent_name)
|
||||
end
|
||||
end.sort.uniq
|
||||
end
|
||||
end
|
||||
|
||||
def update_descendant_names
|
||||
self.descendant_names = descendants
|
||||
end
|
||||
def descendants_of(names)
|
||||
join_recursive do |query|
|
||||
query.start_with(consequent_name: names).connect_by(antecedent_name: :consequent_name)
|
||||
end
|
||||
end
|
||||
|
||||
def update_descendant_names!
|
||||
update_descendant_names
|
||||
update_attribute(:descendant_names, descendant_names)
|
||||
end
|
||||
def tags_implied_by(names)
|
||||
Tag.where(name: active.ancestors_of(names).select(:consequent_name)).where.not(name: names)
|
||||
end
|
||||
|
||||
def update_descendant_names_for_parents
|
||||
parent_implications.each do |parent|
|
||||
parent.update_descendant_names!
|
||||
parent.update_descendant_names_for_parents
|
||||
def tags_implied_to(names)
|
||||
Tag.where(name: active.descendants_of(names).select(:antecedent_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -65,8 +52,9 @@ class TagImplication < TagRelationship
|
||||
def absence_of_circular_relation
|
||||
return if is_rejected?
|
||||
|
||||
# We don't want a -> b && b -> a chains
|
||||
if descendants.include?(antecedent_name)
|
||||
# We don't want a -> b -> a chains
|
||||
implied_tags = TagImplication.tags_implied_by(consequent_name).map(&:name)
|
||||
if implied_tags.include?(antecedent_name)
|
||||
errors[:base] << "Tag implication can not create a circular relation with another tag implication"
|
||||
end
|
||||
end
|
||||
@@ -76,8 +64,9 @@ class TagImplication < TagRelationship
|
||||
return if is_rejected?
|
||||
|
||||
# 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)
|
||||
implications = TagImplication.active.where("NOT (tag_implications.antecedent_name = ? AND tag_implications.consequent_name = ?)", antecedent_name, consequent_name)
|
||||
implied_tags = implications.tags_implied_by(antecedent_name).map(&:name)
|
||||
|
||||
if implied_tags.include?(consequent_name)
|
||||
errors[:base] << "#{antecedent_name} already implies #{consequent_name} through another implication"
|
||||
end
|
||||
@@ -122,7 +111,6 @@ class TagImplication < TagRelationship
|
||||
update(status: "processing")
|
||||
update_posts
|
||||
update(status: "active")
|
||||
update_descendant_names_for_parents
|
||||
forum_updater.update(approval_message(approver), "APPROVED") if update_topic
|
||||
end
|
||||
rescue Exception => e
|
||||
|
||||
Reference in New Issue
Block a user