Files
danbooru/app/models/post.rb
2010-03-12 19:27:54 -05:00

534 lines
15 KiB
Ruby

class Post < ActiveRecord::Base
attr_accessor :updater_id, :updater_ip_addr, :old_tag_string
after_destroy :delete_files
after_destroy :delete_favorites
after_save :create_version
before_save :merge_old_tags
before_save :normalize_tags
before_save :create_tags
before_save :update_tag_post_counts
before_save :set_tag_counts
belongs_to :updater, :class_name => "User"
has_one :unapproval, :dependent => :destroy
has_one :upload, :dependent => :destroy
has_one :moderation_detail, :class_name => "PostModerationDetail", :dependent => :destroy
has_many :versions, :class_name => "PostVersion", :dependent => :destroy
has_many :votes, :class_name => "PostVote", :dependent => :destroy
has_many :notes, :dependent => :destroy
validates_presence_of :updater_id, :updater_ip_addr
attr_accessible :source, :rating, :tag_string, :old_tag_string, :updater_id, :updater_ip_addr, :last_noted_at
module FileMethods
def delete_files
FileUtils.rm_f(file_path)
FileUtils.rm_f(medium_file_path)
FileUtils.rm_f(large_file_path)
FileUtils.rm_f(preview_file_path)
end
def file_path_prefix
Rails.env == "test" ? "test." : ""
end
def file_path
"#{Rails.root}/public/data/original/#{file_path_prefix}#{md5}.#{file_ext}"
end
def medium_file_path
"#{Rails.root}/public/data/medium/#{file_path_prefix}#{md5}.jpg"
end
def large_file_path
"#{Rails.root}/public/data/large/#{file_path_prefix}#{md5}.jpg"
end
def preview_file_path
"#{Rails.root}/public/data/preview/#{file_path_prefix}#{md5}.jpg"
end
def file_url
"/data/original/#{file_path_prefix}#{md5}.#{file_ext}"
end
def medium_file_url
"/data/medium/#{file_path_prefix}#{md5}.jpg"
end
def large_file_url
"/data/large/#{file_path_prefix}#{md5}.jpg"
end
def preview_file_url
"/data/preview/#{file_path_prefix}#{md5}.jpg"
end
def file_url_for(user)
case user.default_image_size
when "medium"
medium_file_url
when "large"
large_file_url
else
file_url
end
end
end
module ImageMethods
def has_medium?
image_width > Danbooru.config.medium_image_width
end
def has_large?
image_width > Danbooru.config.large_image_width
end
end
module ModerationMethods
def unapprove!(reason, current_user, current_ip_addr)
raise Unapproval::Error.new("You can't unapprove a post more than once") if is_flagged?
unapproval = create_unapproval(
:unapprover_id => current_user.id,
:unapprover_ip_addr => current_ip_addr,
:reason => reason
)
if unapproval.errors.any?
raise Unapproval::Error.new(unapproval.errors.full_messages.join("; "))
end
update_attribute(:is_flagged, true)
end
def delete!
update_attribute(:is_deleted, true)
end
def approve!
update_attributes(:is_deleted => false, :is_pending => false)
end
end
module PresenterMethods
def pretty_rating
case rating
when "q"
"Questionable"
when "e"
"Explicit"
when "s"
"Safe"
end
end
end
module VersionMethods
def create_version
version = versions.create(
:source => source,
:rating => rating,
:tag_string => tag_string,
:updater_id => updater_id,
:updater_ip_addr => updater_ip_addr
)
raise PostVersion::Error.new(version.errors.full_messages.join("; ")) if version.errors.any?
end
def revert_to!(version, reverter_id, reverter_ip_addr)
self.source = version.source
self.rating = version.rating
self.tag_string = version.tag_string
self.updater_id = reverter_id
self.updater_ip_addr = reverter_ip_addr
save!
end
end
module TagMethods
def tag_array
@tag_array ||= Tag.scan_tags(tag_string)
end
def tag_array_was
@tag_array_was ||= Tag.scan_tags(tag_string_was)
end
def create_tags
set_tag_string(tag_array.map {|x| Tag.find_or_create_by_name(x).name}.join(" "))
end
def update_tag_post_counts
decrement_tags = tag_array_was - tag_array
increment_tags = tag_array - tag_array_was
execute_sql("UPDATE tags SET post_count = post_count - 1 WHERE name IN (?)", decrement_tags) if decrement_tags.any?
execute_sql("UPDATE tags SET post_count = post_count + 1 WHERE name IN (?)", increment_tags) if increment_tags.any?
end
def set_tag_counts
self.tag_count = 0
self.tag_count_general = 0
self.tag_count_artist = 0
self.tag_count_copyright = 0
self.tag_count_character = 0
categories = Tag.categories_for(tag_array)
categories.each_value do |category|
self.tag_count += 1
case category
when Tag.categories.general
self.tag_count_general += 1
when Tag.categories.artist
self.tag_count_artist += 1
when Tag.categories.copyright
self.tag_count_copyright += 1
when Tag.categories.character
self.tag_count_character += 1
end
end
end
def merge_old_tags
if old_tag_string
# If someone else committed changes to this post before we did,
# then try to merge the tag changes together.
current_tags = tag_array_was()
new_tags = tag_array()
old_tags = Tag.scan_tags(old_tag_string)
set_tag_string(((current_tags + new_tags) - old_tags + (current_tags & new_tags)).uniq.join(" "))
end
end
def reset_tag_array_cache
@tag_array = nil
@tag_array_was = nil
end
def set_tag_string(string)
self.tag_string = string
reset_tag_array_cache
end
def normalize_tags
normalized_tags = Tag.scan_tags(tag_string)
normalized_tags = TagAlias.to_aliased(normalized_tags)
normalized_tags = TagImplication.with_descendants(normalized_tags)
normalized_tags = filter_metatags(normalized_tags)
set_tag_string(normalized_tags.uniq.join(" "))
end
def filter_metatags(tags)
tags.reject {|tag| tag =~ /\A(?:pool|rating|fav|approver|uploader):/}
end
end
module FavoriteMethods
def delete_favorites
Favorite.destroy_all_for_post(self)
end
def add_favorite(user)
self.fav_string += " fav:#{user.name}"
self.fav_string.strip!
Favorite.create(user, self)
end
def remove_favorite(user)
self.fav_string.gsub!(/(?:\A| )fav:#{user.name}(?:\Z| )/, " ")
self.fav_string.strip!
Favorite.destroy(user, self)
end
end
module SearchMethods
class SearchError < Exception ; end
def add_range_relation(arr, field, relation)
case arr[0]
when :eq
relation.where(["#{field} = ?", arr[1]])
when :gt
relation.where(["#{field} > ?", arr[1]])
when :gte
relation.where(["#{field} >= ?", arr[1]])
when :lt
relation.where(["#{field} < ?", arr[1]])
when :lte
relation.where(["#{field} <= ?", arr[1]])
when :between
relation.where(["#{field} BETWEEN ? AND ?", arr[1], arr[2]])
else
relation
end
end
def escape_string_for_tsquery(array)
array.map do |token|
escaped_token = token.gsub(/\\|'/, '\0\0\0\0').gsub("?", "\\\\77").gsub("%", "\\\\37")
"''" + escaped_token + "''"
end
end
def add_tag_string_search_relation(tags, relation)
tag_query_sql = []
if tags[:include].any?
tag_query_sql << "(" + escape_string_for_tsquery(tags[:include]).join(" | ") + ")"
end
if tags[:related].any?
raise SearchError.new("You cannot search for more than #{Danbooru.config.tag_query_limit} tags at a time") if tags[:related].size > Danbooru.config.tag_query_limit
tag_query_sql << "(" + escape_string_for_tsquery(tags[:related]).join(" & ") + ")"
end
if tags[:exclude].any?
raise SearchError.new("You cannot search for more than #{Danbooru.config.tag_query_limit} tags at a time") if tags[:exclude].size > Danbooru.config.tag_query_limit
if tags[:related].any? || tags[:include].any?
tag_query_sql << "!(" + escape_string_for_tsquery(tags[:exclude]).join(" | ") + ")"
else
raise SearchError.new("You cannot search for only excluded tags")
end
end
if tag_query_sql.any?
relation = relation.where("posts.tag_index @@ to_tsquery('danbooru', E'" + tag_query_sql.join(" & ") + "')")
end
relation
end
def add_tag_subscription_relation(subscriptions, relation)
subscriptions.each do |subscription|
subscription =~ /^(.+?):(.+)$/
user_name = $1 || subscription
subscription_name = $2
user = User.find_by_name(user_name)
if user
post_ids = TagSubscription.find_post_ids(user.id, subscription_name)
relation = relation.where(["posts.id IN (?)", post_ids])
end
end
relation
end
def find_by_tags(q, options = {})
unless q.is_a?(Hash)
q = Tag.parse_query(q)
end
relation = where()
relation = add_range_relation(q[:post_id], "posts.id", relation)
relation = add_range_relation(q[:mpixels], "posts.width * posts.height / 1000000.0", relation)
relation = add_range_relation(q[:width], "posts.image_width", relation)
relation = add_range_relation(q[:height], "posts.image_height", relation)
relation = add_range_relation(q[:score], "posts.score", relation)
relation = add_range_relation(q[:filesize], "posts.file_size", relation)
relation = add_range_relation(q[:date], "posts.created_at::date", relation)
relation = add_range_relation(q[:general_tag_count], "posts.tag_count_general", relation)
relation = add_range_relation(q[:artist_tag_count], "posts.tag_count_artist", relation)
relation = add_range_relation(q[:copyright_tag_count], "posts.tag_count_copyright", relation)
relation = add_range_relation(q[:character_tag_count], "posts.tag_count_character", relation)
relation = add_range_relation(q[:tag_count], "posts.tag_count", relation)
if options[:before_id]
relation = relation.where(["posts.id < ?", options[:before_id]])
end
if q[:md5].any?
relation = relation.where(["posts.md5 IN (?)", q[:md5]])
end
if q[:status] == "deleted"
relation = relation.where("posts.is_deleted = TRUE")
elsif q[:status] == "pending"
relation = relation.where("posts.is_pending = TRUE")
elsif q[:status] == "flagged"
relation = relation.where("posts.is_flagged = TRUE")
else
relation = relation.where("posts.is_deleted = FALSE")
end
if q[:source].is_a?(String)
relation = relation.where(["posts.source LIKE ? ESCAPE E'\\\\'", q[:source]])
end
if q[:subscriptions].any?
relation = add_tag_subscription_relation(q[:subscriptions], relation)
end
relation = add_tag_string_search_relation(q[:tags], relation)
if q[:rating] == "q"
relation = relation.where("posts.rating = 'q'")
elsif q[:rating] == "s"
relation = relation.where("posts.rating = 's'")
elsif q[:rating] == "e"
relation = relation.where("posts.rating = 'e'")
end
if q[:rating_negated] == "q"
relation = relation.where("posts.rating <> 'q'")
elsif q[:rating_negated] == "s"
relation = relation.where("posts.rating <> 's'")
elsif q[:rating_negated] == "e"
relation = relation.where("posts.rating <> 'e'")
end
case q[:order]
when "id", "id_asc"
relation = relation.order("posts.id")
when "id_desc"
relation = relation.order("posts.id DESC")
when "score", "score_desc"
relation = relation.order("posts.score DESC, posts.id DESC")
when "score_asc"
relation = relation.order("posts.score, posts.id DESC")
when "mpixels", "mpixels_desc"
# Use "w*h/1000000", even though "w*h" would give the same result, so this can use
# the posts_mpixels index.
relation = relation.order("posts.image_width * posts.image_height / 1000000.0 DESC, posts.id DESC")
when "mpixels_asc"
relation = relation.order("posts.image_width * posts.image_height / 1000000.0, posts.id DESC")
when "portrait"
relation = relation.order("1.0 * image_width / GREATEST(1, image_height), posts.id DESC")
when "landscape"
relation = relation.order("1.0 * image_width / GREATEST(1, image_height) DESC, posts.id DESC")
when "filesize", "filesize_desc"
relation = relation.order("posts.file_size DESC")
when "filesize_asc"
relation = relation.order("posts.file_size")
else
relation = relation.order("posts.id DESC")
end
if options[:limit]
relation = relation.limit(options[:limit])
end
if options[:offset]
relation = relation.offset(options[:offset])
end
relation
end
end
module UploaderMethods
def uploader_id=(user_id)
self.uploader = User.find(user_id)
end
def uploader_id
uploader.id
end
def uploader_name
uploader_string[9..-1]
end
def uploader
User.find_by_name(uploader_name)
end
def uploader=(user)
self.uploader_string = "uploader:#{user.name}"
end
end
module PoolMethods
def add_pool(pool)
self.pool_string += " pool:#{pool.name}"
self.pool_string.strip!
pool.add_post!(self)
end
def remove_pool(pool)
self.pool_string.gsub!(/(?:\A| )pool:#{pool.name}(?:\Z| )/, " ")
self.pool_string.strip!
pool.remove_post!(self)
end
end
module VoteMethods
def can_be_voted_by?(user)
!votes.exists?(["user_id = ?", user.id])
end
def vote!(user, is_positive)
if can_be_voted_by?(user)
if is_positive
increment!(:score)
else
decrement!(:score)
end
votes.create(:user_id => user.id)
else
raise PostVote::Error.new("You have already voted for this comment")
end
end
end
module CountMethods
def fast_count(tags)
Cache.get("pfc:#{Cache.sanitize(tags)}", 24.hours) do
Post.find_by_tags(tags).count
end
end
def fast_delete_count(tags)
Cache.get("pfdc:#{Cache.sanitize(tags)}", 24.hours) do
Post.find_by_tags("#{tags} status:deleted").count
end
end
end
include FileMethods
include ImageMethods
include ModerationMethods
include PresenterMethods
include VersionMethods
include TagMethods
include FavoriteMethods
include UploaderMethods
include PoolMethods
extend SearchMethods
include VoteMethods
extend CountMethods
def reload(options = nil)
super
reset_tag_array_cache
end
end