Files
danbooru/app/logical/bulk_update_request_processor.rb
evazion 1ddcc661e1 BURs: clean up parsing and error handling.
* Don't raise exceptions when a BUR is invalid. Instead, use Rails
  validations to return errors. Fixes invalid BURs potentially raising
  exceptions in views. Also makes it so that each error in a BUR is
  reported, not just the first one.

* Revalidate the BUR whenever the script is edited, not just when the
  BUR is created. Ensures the BUR can't be broken by editing. Fixes a bug
  where forum threads could be broken by someone editing a BUR and
  breaking the syntax, thereby causing the BUR to raise an unparseable
  script error when the forum thread was viewed.

* Validate that removed aliases and implication actually exist.

* Validate that the tag actually exists when changing a tag's category.

* Combine bulk update request processor unit tests with main bulk update
  request unit tests.
2020-08-24 17:10:48 -05:00

176 lines
6.4 KiB
Ruby

class BulkUpdateRequestProcessor
include ActiveModel::Validations
class Error < StandardError; end
attr_reader :bulk_update_request
delegate :script, :forum_topic_id, :skip_secondary_validations, to: :bulk_update_request
validate :validate_script
def initialize(bulk_update_request)
@bulk_update_request = bulk_update_request
end
# Parse the script into a list of commands.
def commands
script.split(/\r\n|\r|\n/).reject(&:blank?).map do |line|
line = line.gsub(/[[:space:]]+/, " ").strip
next if line.empty?
case line
when /\A(?:create alias|aliasing|alias) (\S+) -> (\S+)\z/i
[:create_alias, $1, $2]
when /\A(?:create implication|implicating|implicate|imply) (\S+) -> (\S+)\z/i
[:create_implication, $1, $2]
when /\A(?:remove alias|unaliasing|unalias) (\S+) -> (\S+)\z/i
[:remove_alias, $1, $2]
when /\A(?:remove implication|unimplicating|unimplicate|unimply) (\S+) -> (\S+)\z/i
[:remove_implication, $1, $2]
when /\A(?:mass update|updating|update|change) (.+?) -> (.*)\z/i
[:mass_update, $1, $2]
when /\Acategory (\S+) -> (#{Tag.categories.regexp})\z/i
[:change_category, $1, $2]
else
[:invalid_line, line]
end
end
end
def validate_script
commands.each do |command, *args|
case command
when :create_alias
tag_alias = TagAlias.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], skip_secondary_validations: skip_secondary_validations)
if tag_alias.invalid?
errors[:base] << "Can't create alias #{tag_alias.antecedent_name} -> #{tag_alias.consequent_name} (#{tag_alias.errors.full_messages.join("; ")})"
end
when :create_implication
tag_implication = TagImplication.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], skip_secondary_validations: skip_secondary_validations)
if tag_implication.invalid?
errors[:base] << "Can't create implication #{tag_implication.antecedent_name} -> #{tag_implication.consequent_name} (#{tag_implication.errors.full_messages.join("; ")})"
end
when :remove_alias
tag_alias = TagAlias.active.find_by(antecedent_name: args[0], consequent_name: args[1])
if tag_alias.nil?
errors[:base] << "Can't remove alias #{args[0]} -> #{args[1]} (alias doesn't exist)"
end
when :remove_implication
tag_implication = TagImplication.active.find_by(antecedent_name: args[0], consequent_name: args[1])
if tag_implication.nil?
errors[:base] << "Can't remove implication #{args[0]} -> #{args[1]} (implication doesn't exist)"
end
when :change_category
tag = Tag.find_by_name(args[0])
if tag.nil?
errors[:base] << "Can't change category #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)"
end
when :mass_update
# okay
when :invalid_line
errors[:base] << "Invalid line: #{args[0]}"
else
# should never happen
raise Error, "Unknown command: #{command}"
end
end
end
def process!(approver)
ActiveRecord::Base.transaction do
validate!
commands.map do |command, *args|
case command
when :create_alias
tag_alias = TagAlias.create!(creator: approver, forum_topic_id: forum_topic_id, status: "pending", antecedent_name: args[0], consequent_name: args[1], skip_secondary_validations: skip_secondary_validations)
tag_alias.approve!(approver: approver)
when :create_implication
tag_implication = TagImplication.create!(creator: approver, forum_topic_id: forum_topic_id, status: "pending", antecedent_name: args[0], consequent_name: args[1], skip_secondary_validations: skip_secondary_validations)
tag_implication.approve!(approver: approver)
when :remove_alias
tag_alias = TagAlias.active.find_by!(antecedent_name: args[0], consequent_name: args[1])
tag_alias.reject!
when :remove_implication
tag_implication = TagImplication.active.find_by!(antecedent_name: args[0], consequent_name: args[1])
tag_implication.reject!
when :mass_update
TagBatchChangeJob.perform_later(args[0], args[1], User.system, "127.0.0.1")
when :change_category
tag = Tag.find_or_create_by_name(args[0])
tag.update!(category: Tag.categories.value_for(args[1]))
else
# should never happen
raise Error, "Unknown command: #{command}"
end
end
end
end
def affected_tags
commands.flat_map do |command, *args|
case command
when :create_alias, :remove_alias, :create_implication, :remove_implication
[args[0], args[1]]
when :mass_update
tags = PostQueryBuilder.new(args[0]).tags + PostQueryBuilder.new(args[1]).tags
tags.reject(&:negated).reject(&:optional).reject(&:wildcard).map(&:name)
when :change_category
args[0]
end
end.sort.uniq
end
def is_tag_move_allowed?
commands.all? do |command, *args|
case command
when :create_alias
BulkUpdateRequestProcessor.is_tag_move_allowed?(args[0], args[1])
when :mass_update
lhs = PostQueryBuilder.new(args[0])
rhs = PostQueryBuilder.new(args[1])
lhs.is_simple_tag? && rhs.is_simple_tag? && BulkUpdateRequestProcessor.is_tag_move_allowed?(args[0], args[1])
else
false
end
end
end
def to_dtext
commands.map do |command, *args|
case command
when :create_alias, :create_implication, :remove_alias, :remove_implication
"#{command.to_s.tr("_", " ")} [[#{args[0]}]] -> [[#{args[1]}]]"
when :mass_update
"mass update {{#{args[0]}}} -> #{args[1]}"
when :change_category
"category [[#{args[0]}]] -> #{args[1]}"
else
# should never happen
raise Error, "Unknown command: #{command}"
end
end.join("\n")
end
def self.is_tag_move_allowed?(antecedent_name, consequent_name)
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name))
(antecedent_tag.blank? || antecedent_tag.empty? || (antecedent_tag.artist? && antecedent_tag.post_count <= 100)) &&
(consequent_tag.blank? || consequent_tag.empty? || (consequent_tag.artist? && consequent_tag.post_count <= 100))
end
end