Drop the ability to write e.g. `create alias foo -> char:bar` in a BUR to change the tag's type as a side effect. You can only use these tag type prefixes in tag edits now. This feature was only intended to be used in tag edits. The fact it worked elsewhere was unintended behavior. This feature was problematic because it relied on `Tag.find_or_create_by_name` automagically changing the tag's category when the tag name contained a tag category prefix, e.g. `char:hatsune_miku`. This meant that merely looking up a tag could have the side effect of changing its category. It was also bad because `find_or_create_by_name` had a hidden dependency on the current user, which may not be set or available in all contexts.
336 lines
9.8 KiB
Ruby
336 lines
9.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Artist < ApplicationRecord
|
|
extend Memoist
|
|
class RevertError < StandardError; end
|
|
|
|
attr_accessor :url_string_changed
|
|
|
|
deletable
|
|
|
|
normalize :name, :normalize_name
|
|
normalize :group_name, :normalize_other_name
|
|
normalize :other_names, :normalize_other_names
|
|
array_attribute :other_names # XXX must come after `normalize :other_names`
|
|
|
|
validate :validate_artist_name
|
|
validates :name, tag_name: true, uniqueness: true
|
|
after_validation :add_url_warnings
|
|
|
|
before_save :update_tag_category
|
|
after_save :create_version
|
|
after_save :clear_url_string_changed
|
|
|
|
has_many :members, :class_name => "Artist", :foreign_key => "group_name", :primary_key => "name"
|
|
has_many :urls, dependent: :destroy, class_name: "ArtistURL", autosave: true
|
|
has_many :versions, -> {order("artist_versions.id ASC")}, :class_name => "ArtistVersion"
|
|
has_one :wiki_page, -> { active }, foreign_key: "title", primary_key: "name"
|
|
has_one :tag_alias, -> { active }, foreign_key: "antecedent_name", primary_key: "name"
|
|
belongs_to :tag, foreign_key: "name", primary_key: "name", default: -> { Tag.new(name: name, category: Tag.categories.artist) }
|
|
|
|
scope :banned, -> { where(is_banned: true) }
|
|
scope :unbanned, -> { where(is_banned: false) }
|
|
|
|
module UrlMethods
|
|
extend ActiveSupport::Concern
|
|
|
|
def sorted_urls
|
|
urls.sort_by do |url|
|
|
[url.is_active? ? 0 : 1, url.priority, url.domain, url.secondary_url? ? 1 : 0, url.url]
|
|
end
|
|
end
|
|
|
|
def url_array
|
|
urls.map(&:to_s).sort
|
|
end
|
|
|
|
def url_string
|
|
url_array.join("\n")
|
|
end
|
|
|
|
def url_string=(string)
|
|
url_string_was = url_string
|
|
|
|
self.urls = string.to_s.scan(/[^[:space:]]+/).map do |url|
|
|
is_active, url = ArtistURL.parse_prefix(url)
|
|
self.urls.find_or_initialize_by(url: url, is_active: is_active)
|
|
end.uniq(&:url)
|
|
|
|
self.url_string_changed = (url_string_was != url_string)
|
|
end
|
|
|
|
def clear_url_string_changed
|
|
self.url_string_changed = false
|
|
end
|
|
|
|
class_methods do
|
|
# Find all artist URLs matching `regex`, and replace the `from` regex with the `to` string.
|
|
def rewrite_urls(regex, from, to)
|
|
Artist.joins(:urls).where_regex("artist_urls.url", regex).find_each do |artist|
|
|
artist.update!(url_string: artist.url_string.gsub(from, to))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :NameMethods do
|
|
class_methods do
|
|
def normalize_name(name)
|
|
name.to_s.downcase.strip.gsub(/ /, "_").to_s
|
|
end
|
|
|
|
def normalize_other_names(other_names)
|
|
other_names.map { |name| normalize_other_name(name) }.uniq.reject(&:blank?)
|
|
end
|
|
|
|
# XXX Differences from wiki page other names: allow uppercase, use NFC
|
|
# instead of NFKC, and allow repeated, leading, and trailing underscores.
|
|
def normalize_other_name(other_name)
|
|
other_name.to_s.unicode_normalize(:nfc).normalize_whitespace.squish.tr(" ", "_")
|
|
end
|
|
end
|
|
|
|
def pretty_name
|
|
name.tr("_", " ")
|
|
end
|
|
end
|
|
|
|
module VersionMethods
|
|
def create_version(force = false)
|
|
if saved_change_to_name? || url_string_changed || saved_change_to_is_deleted? || saved_change_to_is_banned? || saved_change_to_other_names? || saved_change_to_group_name? || force
|
|
if merge_version?
|
|
merge_version
|
|
else
|
|
create_new_version
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_new_version
|
|
ArtistVersion.create(
|
|
:artist_id => id,
|
|
:name => name,
|
|
:updater_id => CurrentUser.id,
|
|
:updater_ip_addr => CurrentUser.ip_addr,
|
|
:urls => url_array,
|
|
:is_deleted => is_deleted,
|
|
:is_banned => is_banned,
|
|
:other_names => other_names,
|
|
:group_name => group_name
|
|
)
|
|
end
|
|
|
|
def merge_version
|
|
prev = versions.last
|
|
prev.update(name: name, urls: url_array, is_deleted: is_deleted, is_banned: is_banned, other_names: other_names, group_name: group_name)
|
|
end
|
|
|
|
def merge_version?
|
|
prev = versions.last
|
|
prev && prev.updater_id == CurrentUser.user.id && prev.updated_at > 1.hour.ago
|
|
end
|
|
|
|
def revert_to!(version)
|
|
if id != version.artist_id
|
|
raise RevertError.new("You cannot revert to a previous version of another artist.")
|
|
end
|
|
|
|
self.name = version.name
|
|
self.url_string = version.urls.join("\n")
|
|
self.is_deleted = version.is_deleted
|
|
self.other_names = version.other_names
|
|
self.group_name = version.group_name
|
|
save
|
|
end
|
|
end
|
|
|
|
module FactoryMethods
|
|
# Make a new artist, fetching the defaults either from the given source, or
|
|
# from the source of the artist's last upload.
|
|
def new_with_defaults(params)
|
|
source = params.delete(:source)
|
|
|
|
if source.blank? && params[:name].present?
|
|
post = Post.system_tag_match("source:http* #{params[:name]}").first
|
|
source = post.try(:source)
|
|
end
|
|
|
|
if source.present?
|
|
artist = Source::Extractor.find(source).new_artist
|
|
artist.attributes = params
|
|
else
|
|
artist = Artist.new(params)
|
|
end
|
|
|
|
artist
|
|
end
|
|
end
|
|
|
|
module TagMethods
|
|
def validate_artist_name
|
|
return unless !is_deleted? && name_changed?
|
|
|
|
if tag.present? && tag.category_name != "Artist" && !tag.empty?
|
|
errors.add(:name, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
|
|
end
|
|
|
|
if tag&.is_deprecated?
|
|
errors.add(:name, "'#{name}' is an ambiguous tag; try another name")
|
|
end
|
|
|
|
if tag_alias.present?
|
|
errors.add(:name, "'#{name}' is aliased to '#{tag_alias.consequent_name}'")
|
|
end
|
|
end
|
|
|
|
def update_tag_category
|
|
return unless !is_deleted? && name_changed? && tag.present?
|
|
|
|
if tag.category_name != "Artist" && tag.empty?
|
|
tag.update!(category: Tag.categories.artist)
|
|
end
|
|
end
|
|
end
|
|
|
|
module BanMethods
|
|
def unban!
|
|
Post.transaction do
|
|
ti = TagImplication.find_by(antecedent_name: name, consequent_name: "banned_artist")
|
|
ti&.destroy
|
|
|
|
Post.raw_tag_match(name).find_each do |post|
|
|
post.unban!
|
|
fixed_tags = post.tag_string.sub(/(?:\A| )banned_artist(?:\Z| )/, " ").strip
|
|
post.update(tag_string: fixed_tags)
|
|
end
|
|
|
|
update!(is_banned: false)
|
|
ModAction.log("unbanned artist ##{id}", :artist_unban)
|
|
end
|
|
end
|
|
|
|
def ban!(banner: CurrentUser.user)
|
|
Post.transaction do
|
|
Post.raw_tag_match(name).each(&:ban!)
|
|
|
|
# potential race condition but unlikely
|
|
unless TagImplication.where(:antecedent_name => name, :consequent_name => "banned_artist").exists?
|
|
Tag.find_or_create_by_name("banned_artist", category: "artist", current_user: banner)
|
|
TagImplication.approve!(antecedent_name: name, consequent_name: "banned_artist", approver: banner)
|
|
end
|
|
|
|
update!(is_banned: true)
|
|
ModAction.log("banned artist ##{id}", :artist_ban)
|
|
end
|
|
end
|
|
end
|
|
|
|
module SearchMethods
|
|
def name_matches(query)
|
|
where_like(:name, normalize_name(query))
|
|
end
|
|
|
|
def any_other_name_matches(regex)
|
|
where(id: Artist.from("unnest(other_names) AS other_name").where_regex("other_name", regex))
|
|
end
|
|
|
|
def any_other_name_like(name)
|
|
where(id: Artist.from("unnest(other_names) AS other_name").where_like("other_name", name))
|
|
end
|
|
|
|
def any_name_matches(query)
|
|
if query =~ %r{\A/(.*)/\z}
|
|
where_regex(:name, $1).or(any_other_name_matches($1)).or(where_regex(:group_name, $1))
|
|
else
|
|
normalized_name = normalize_name(query)
|
|
normalized_name = "*#{normalized_name}*" unless normalized_name.include?("*")
|
|
where_like(:name, normalized_name).or(any_other_name_like(normalized_name)).or(where_like(:group_name, normalized_name))
|
|
end
|
|
end
|
|
|
|
def urls_match(urls)
|
|
urls = Array.wrap(urls).flat_map(&:split)
|
|
return all if urls.empty?
|
|
|
|
urls.map do |url|
|
|
url_matches(url)
|
|
end.reduce(&:or)
|
|
end
|
|
|
|
def url_matches(query)
|
|
query = query.strip
|
|
|
|
if query =~ %r{\Ahttps?://}i
|
|
url = Source::Extractor.find(query).profile_url || query
|
|
ArtistFinder.find_artists(url)
|
|
else
|
|
where(id: ArtistURL.url_matches(query).select(:artist_id))
|
|
end
|
|
end
|
|
|
|
def any_name_or_url_matches(query)
|
|
query = query.strip
|
|
|
|
if query =~ %r{\Ahttps?://}i
|
|
url_matches(query)
|
|
else
|
|
any_name_matches(query)
|
|
end
|
|
end
|
|
|
|
def search(params)
|
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :other_names, :urls, :wiki_page, :tag_alias, :tag)
|
|
|
|
if params[:any_other_name_like]
|
|
q = q.any_other_name_like(params[:any_other_name_like])
|
|
end
|
|
|
|
if params[:any_name_matches].present?
|
|
q = q.any_name_matches(params[:any_name_matches])
|
|
end
|
|
|
|
if params[:any_name_or_url_matches].present?
|
|
q = q.any_name_or_url_matches(params[:any_name_or_url_matches])
|
|
end
|
|
|
|
if params[:url_matches].present?
|
|
q = q.urls_match(params[:url_matches])
|
|
end
|
|
|
|
case params[:order]
|
|
when "name"
|
|
q = q.order("artists.name")
|
|
when "updated_at"
|
|
q = q.order("artists.updated_at desc")
|
|
when "post_count"
|
|
q = q.left_outer_joins(:tag).order("tags.post_count desc nulls last").order("artists.name")
|
|
else
|
|
q = q.apply_default_order(params)
|
|
end
|
|
|
|
q
|
|
end
|
|
end
|
|
|
|
def add_url_warnings
|
|
urls.each do |url|
|
|
warnings.add(:base, url.warnings.full_messages.join("; ")) if url.warnings.any?
|
|
end
|
|
end
|
|
|
|
include UrlMethods
|
|
include VersionMethods
|
|
extend FactoryMethods
|
|
include TagMethods
|
|
include BanMethods
|
|
extend SearchMethods
|
|
|
|
def self.model_restriction(table)
|
|
super.where(table[:is_deleted].eq(false))
|
|
end
|
|
|
|
def self.available_includes
|
|
[:members, :urls, :wiki_page, :tag_alias, :tag]
|
|
end
|
|
end
|