Files
danbooru/app/models/post.rb
2010-02-15 17:45:09 -05:00

514 lines
14 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_many :versions, :class_name => "PostVersion", :dependent => :destroy
has_many :votes, :class_name => "PostVote", :dependent => :destroy
attr_accessible :source, :rating, :tag_string, :old_tag_string
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(thumb_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 thumb_file_path
"#{Rails.root}/public/data/thumb/#{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 thumb_file_url
"/data/thumb/#{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)
self.source = version.source
self.rating = version.rating
self.tag_string = version.tag_string
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
include FileMethods
include ImageMethods
include ModerationMethods
include PresenterMethods
include VersionMethods
include TagMethods
include FavoriteMethods
include UploaderMethods
include PoolMethods
extend SearchMethods
include VoteMethods
def reload(options = nil)
super
reset_tag_array_cache
end
end