Normalize artist group names following the same rules as artist other names. This means artist group names now use underscores instead of spaces. It also means extra space characters at the beginning and end of names is stripped, and Unicode characters are normalized. Fixes #4647, which was caused by users accidentally replacing group names with a single space character when trying to remove a group.
304 lines
8.9 KiB
Ruby
304 lines
8.9 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_tag_category
|
|
validates :name, tag_name: true, uniqueness: true
|
|
|
|
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, :foreign_key => "title", :primary_key => "name"
|
|
has_one :tag_alias, :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 {|a, b| a.priority <=> b.priority}
|
|
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
|
|
end
|
|
|
|
concerning :NameMethods do
|
|
class_methods do
|
|
def normalize_name(name)
|
|
name.to_s.mb_chars.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 = Sources::Strategies.find(source).new_artist
|
|
artist.attributes = params
|
|
else
|
|
artist = Artist.new(params)
|
|
end
|
|
|
|
artist
|
|
end
|
|
end
|
|
|
|
module TagMethods
|
|
def validate_tag_category
|
|
return unless !is_deleted? && name_changed? && tag.present?
|
|
|
|
if 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
|
|
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("artist:banned_artist") # ensure the banned_artist exists and is an artist tag.
|
|
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 url_matches(query)
|
|
query = query.strip
|
|
|
|
if query =~ %r{\A/(.*)/\z}
|
|
where(id: ArtistUrl.where_regex(:url, $1).select(:artist_id))
|
|
elsif query.include?("*")
|
|
where(id: ArtistUrl.where_like(:url, query).select(:artist_id))
|
|
elsif query =~ %r{\Ahttps?://}i
|
|
ArtistFinder.find_artists(query)
|
|
else
|
|
where(id: ArtistUrl.where_like(:url, "*#{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.url_matches(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
|
|
|
|
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
|