Files
danbooru/app/models/artist.rb
evazion f38c38f26e search: split tag_match into user_tag_match / system_tag_match.
When doing a tag search, we have to be careful about which user we're
running the search as because the results depend on the current user.
Specifically, things like private favorites, private favorite groups,
post votes, saved searches, and flagger names depend on the user's
permissions, and whether non-safe or deleted posts are filtered out
depend on whether the user has safe mode on or the hide deleted posts
setting enabled.

* Refactor internal searches to explicitly state whether they're
  running as the system user (DanbooruBot) or as the current user.
* Explicitly pass in the current user to PostQueryBuilder instead of
  implicitly relying on the CurrentUser global.
* Get rid of CurrentUser.admin_mode? (used to ignore the hide deleted
  post setting) and CurrentUser.without_safe_mode (used to ignore safe
  mode).
* Change the /counts/posts.json endpoint to ignore safe mode and the
  hide deleted posts settings when counting posts.
* Fix searches not correctly overriding the hide deleted posts setting
  when multiple status: metatags were used (e.g. `status:banned status:active`)
* Fix fast_count not respecting the hide deleted posts setting when the
  status:banned metatag was used.
2020-05-07 03:29:44 -05:00

308 lines
8.8 KiB
Ruby

class Artist < ApplicationRecord
extend Memoist
class RevertError < StandardError; end
attr_accessor :url_string_changed
array_attribute :other_names
deletable
before_validation :normalize_name
before_validation :normalize_other_names
after_save :create_version
after_save :clear_url_string_changed
validate :validate_tag_category
validates :name, tag_name: true, uniqueness: true
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) }
accepts_nested_attributes_for :wiki_page, update_only: true, reject_if: :all_blank
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
module NameMethods
extend ActiveSupport::Concern
module ClassMethods
def normalize_name(name)
name.to_s.mb_chars.downcase.strip.gsub(/ /, '_').to_s
end
end
def normalize_name
self.name = Artist.normalize_name(name)
end
def pretty_name
name.tr("_", " ")
end
def normalize_other_names
self.other_names = other_names.map { |x| Artist.normalize_name(x) }.uniq
self.other_names -= [name]
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.normalize_name
artist.normalize_other_names
artist
end
end
module TagMethods
def validate_tag_category
return unless !is_deleted? && name_changed?
if tag.category_name == "General"
tag.update(category: Tag.categories.artist)
elsif tag.category_name != "Artist"
errors[:base] << "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags"
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_implication = TagImplication.create!(antecedent_name: name, consequent_name: "banned_artist", skip_secondary_validations: true, creator: banner)
tag_implication.approve!(approver: banner)
end
update!(is_banned: true)
ModAction.log("banned artist ##{id}", :artist_ban)
end
end
end
module SearchMethods
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 = super
q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :other_names)
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
if params[:has_tag].to_s.truthy?
q = q.where(name: Tag.nonempty.select(:name))
elsif params[:has_tag].to_s.falsy?
q = q.where.not(name: Tag.nonempty.select(:name))
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 NameMethods
include VersionMethods
extend FactoryMethods
include TagMethods
include BanMethods
extend SearchMethods
def status
if is_banned? && !is_deleted?
"Banned"
elsif is_banned?
"Banned Deleted"
elsif is_deleted?
"Deleted"
else
"Active"
end
end
def self.available_includes
[:members, :urls, :wiki_page, :tag_alias, :tag]
end
end