Files
danbooru/app/models/artist.rb
evazion 57bb51621e Add debugger gem.
Fix random VCR failures in Pixiv tests.

Sometimes tests randomly fail because the PHPSESSID they use in their
HTTP requests to Pixiv is different than the one that was originally
recorded by VCR. This causes VCR to complain that the requests don't
match.

This is caused by the PHPSESSID being globally cached in Memcache.
Depending on the order the tests run in (which is random), one set of
tests can use a PHPSESSID that was recorded for a /different/ set of
tests.

Improve Pixiv URL matching.

* Allow URLs that are missing the http:// part. These are sometimes seen
  in artist entries.
* Ignore URLs from random Pixiv domains such as dic.pixiv.net,
  blog.pixiv.net, etc. These are also sometimes in artist entries.

Improve normalize_for_artist_finder! URL matching.

* Normalize www.pixiv.net/stacc/username URLs.
* Correctly normalize URLs that are missing the illust ID part on the end
  (i.e. http://i2.pixiv.net/img04/img/syounen_no_uta/). These are common
  in artist entries.

Match URLs strictly when normalizing for artist entries.

Only normalize Pixiv URLs that strictly match a known format. Pass any
unrecognized URLs through without attempting to normalize them, just to
be safe.

Normalize URLs when saving artist entries.
2014-12-03 13:16:05 -08:00

460 lines
12 KiB
Ruby

class Artist < ActiveRecord::Base
before_create :initialize_creator
before_validation :normalize_name
after_save :create_version
after_save :save_url_string
after_save :categorize_tag
validates_uniqueness_of :name
belongs_to :creator, :class_name => "User"
has_many :members, :class_name => "Artist", :foreign_key => "group_name", :primary_key => "name"
has_many :urls, :dependent => :destroy, :class_name => "ArtistUrl"
has_many :versions, lambda {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"
has_one :tag, :foreign_key => "name", :primary_key => "name"
accepts_nested_attributes_for :wiki_page
attr_accessible :body, :name, :url_string, :other_names, :other_names_comma, :group_name, :wiki_page_attributes, :notes, :as => [:member, :gold, :builder, :platinum, :contributor, :janitor, :moderator, :default, :admin]
attr_accessible :is_active, :as => [:builder, :contributor, :janitor, :moderator, :default, :admin]
attr_accessible :is_banned, :as => :admin
module UrlMethods
extend ActiveSupport::Concern
module ClassMethods
def find_all_by_url(url)
url = ArtistUrl.normalize(url)
artists = []
# return [] unless Sources::Site.new(url).normalized_for_artist_finder?
while artists.empty? && url.size > 10
u = url.sub(/\/+$/, "") + "/"
u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%'
artists += Artist.joins(:urls).where(["artists.is_active = TRUE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all
url = File.dirname(url) + "/"
break if url =~ /pixiv\.net\/(?:img\/)?$/i
break if url =~ /lohas\.nicoseiga\.jp\/priv\/$/i
break if url =~ /(?:data|media)\.tumblr\.com\/[a-z0-9]+\/$/i
break if url =~ /deviantart\.net\//i
end
artists.inject({}) {|h, x| h[x.name] = x; h}.values.slice(0, 20)
end
end
def save_url_string
if @url_string
urls.clear
@url_string.scan(/\S+/).each do |url|
urls.create(:url => url)
end
end
end
def url_string=(string)
@url_string = string
end
def url_string
@url_string || urls.map {|x| x.url}.join("\n")
end
def url_string_changed?
url_string.scan(/\S+/) != urls.map(&:url)
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 other_names_array
other_names.try(:split, /\s/)
end
def other_names_comma
other_names_array.try(:join, ", ")
end
def other_names_comma=(string)
self.other_names = string.split(/,/).map {|x| Artist.normalize_name(x)}.join(" ")
end
end
module GroupMethods
def member_names
members.map(&:name).join(", ")
end
end
module VersionMethods
def create_version(force=false)
if name_changed? || url_string_changed? || is_active_changed? || is_banned_changed? || other_names_changed? || group_name_changed? || notes_changed? || 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.user.id,
:updater_ip_addr => CurrentUser.ip_addr,
:url_string => url_string,
:is_active => is_active,
:is_banned => is_banned,
:other_names => other_names,
:group_name => group_name
)
end
def merge_version
prev = versions.last
prev.update_attributes(
:name => name,
:url_string => url_string,
:is_active => is_active,
: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)
self.name = version.name
self.url_string = version.url_string
self.is_active = version.is_active
self.other_names = version.other_names
self.group_name = version.group_name
save
end
end
module FactoryMethods
def new_with_defaults(params)
Artist.new.tap do |artist|
if params[:name]
artist.name = params[:name]
post = CurrentUser.without_safe_mode do
Post.tag_match("source:http #{artist.name}").first
end
unless post.nil? || post.source.blank?
artist.url_string = post.source
end
end
if params[:other_names]
artist.other_names = params[:other_names]
end
if params[:urls]
artist.url_string = params[:urls]
end
end
end
end
module NoteMethods
def notes
if wiki_page
wiki_page.body
else
nil
end
end
def notes=(msg)
if name_changed? && name_was.present?
old_wiki_page = WikiPage.titled(name_was).first
end
if wiki_page
if name_changed? && name_was.present?
wiki_page.body = wiki_page.body + "\n\n" + msg
else
wiki_page.body = msg
end
if wiki_page.body_changed?
wiki_page.save
@notes_changed = true
end
elsif old_wiki_page
old_wiki_page.title = name
old_wiki_page.body = msg
if old_wiki_page.body_changed? || old_wiki_page.title_changed?
old_wiki_page.save
@notes_changed = true
end
elsif msg.present?
self.wiki_page = WikiPage.new(:title => name, :body => msg)
@notes_changed = true
end
end
def notes_changed?
!!@notes_changed
end
end
module TagMethods
def has_tag_alias?
TagAlias.active.exists?(["antecedent_name = ?", name])
end
def tag_alias_name
TagAlias.active.find_by_antecedent_name(name).consequent_name
end
def category_name
Tag.category_for(name)
end
def categorize_tag
if new_record? || name_changed?
Tag.find_or_create_by_name("artist:#{name}")
end
end
end
module BanMethods
def unban!
Post.transaction do
CurrentUser.without_safe_mode do
ti = TagImplication.where(:antecedent_name => name, :consequent_name => "banned_artist").first
ti.destroy if ti
begin
Post.tag_match(name).each do |post|
post.unban!
fixed_tags = post.tag_string.sub(/(?:\A| )banned_artist(?:\Z| )/, " ").strip
post.update_attributes(:tag_string => fixed_tags)
end
rescue Post::SearchError
# swallow
end
update_column(:is_banned, false)
end
end
end
def ban!
Post.transaction do
CurrentUser.without_safe_mode do
begin
Post.tag_match(name).each do |post|
post.ban!
end
rescue Post::SearchError
# swallow
end
# 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")
tag_implication.delay(:queue => "default").process!
end
update_column(:is_banned, true)
end
end
end
end
module SearchMethods
def active
where("is_active = true")
end
def banned
where("is_banned = true")
end
def url_matches(string)
matches = find_all_by_url(string).map(&:id)
if matches.any?
where("id in (?)", matches)
else
where("false")
end
end
def other_names_match(string)
if string =~ /\*/ && CurrentUser.user.is_builder?
where("other_names ILIKE ? ESCAPE E'\\\\'", string.to_escaped_for_sql_like)
else
where("other_names_index @@ to_tsquery('danbooru', E?)", Artist.normalize_name(string).to_escaped_for_tsquery)
end
end
def group_name_matches(name)
stripped_name = normalize_name(name).to_escaped_for_sql_like
where("group_name LIKE ? ESCAPE E'\\\\'", stripped_name)
end
def name_matches(name)
stripped_name = normalize_name(name).to_escaped_for_sql_like
where("name LIKE ? ESCAPE E'\\\\'", stripped_name)
end
def named(name)
where("name = ?", normalize_name(name))
end
def any_name_matches(name)
stripped_name = normalize_name(name).to_escaped_for_sql_like
if name =~ /\*/ && CurrentUser.user.is_builder?
where("(name LIKE ? ESCAPE E'\\\\' OR other_names LIKE ? ESCAPE E'\\\\')", stripped_name, stripped_name)
else
name_for_tsquery = normalize_name(name).to_escaped_for_tsquery
where("(name LIKE ? ESCAPE E'\\\\' OR other_names_index @@ to_tsquery('danbooru', E?))", stripped_name, name_for_tsquery)
end
end
def search(params)
q = where("true")
params = {} if params.blank?
case params[:name]
when /^http/
q = q.url_matches(params[:name])
when /name:(.+)/
q = q.name_matches($1)
when /other:(.+)/
q = q.other_names_match($1)
when /group:(.+)/
q = q.group_name_matches($1)
when /status:banned/
q = q.banned
when /status:active/
q = q.where("is_banned = false and is_active = true")
when /./
q = q.any_name_matches(params[:name])
end
params[:order] ||= params.delete(:sort)
case params[:order]
when "name"
q = q.reorder("name")
when "updated_at"
q = q.reorder("updated_at desc")
else
q = q.reorder("id desc")
end
if params[:is_active] == "true"
q = q.active
elsif params[:is_active] == "false"
q = q.where("is_active = false")
end
if params[:is_banned] == "true"
q = q.banned
elsif params[:is_banned] == "false"
q = q.where("is_banned = false")
end
if params[:id].present?
q = q.where("id = ?", params[:id])
end
if params[:creator_name].present?
q = q.where("creator_id = (select _.id from users _ where lower(_.name) = ?)", params[:creator_name].tr(" ", "_").mb_chars.downcase)
end
if params[:creator_id].present?
q = q.where("creator_id = ?", params[:creator_id].to_i)
end
if params[:empty_only] == "true"
q = q.joins(:tag).where("tags.post_count = 0")
end
q
end
end
module ApiMethods
def hidden_attributes
super + [:other_names_index]
end
def legacy_api_hash
return {
:id => id,
:name => name,
:other_names => other_names,
:group_name => group_name,
:urls => artist_urls.map {|x| x.url},
:is_active => is_active?,
:updater_id => 0
}
end
end
include UrlMethods
include NameMethods
include GroupMethods
include VersionMethods
extend FactoryMethods
include NoteMethods
include TagMethods
include BanMethods
extend SearchMethods
include ApiMethods
def status
if is_banned? && is_active?
"Banned"
elsif is_banned?
"Banned Deleted"
elsif is_active?
"Active"
else
"Deleted"
end
end
def initialize_creator
self.creator_id = CurrentUser.user.id
end
def deletable_by?(user)
user.is_builder?
end
def visible?
!is_banned? || CurrentUser.user.is_janitor?
end
end