fixed tag and pending post tests, added category multiget helper to tag, post/unapproval/post version in progress still

This commit is contained in:
albert
2010-02-10 16:12:30 -05:00
parent ef8be1a500
commit e6888ea1dd
19 changed files with 1077 additions and 319 deletions

View File

@@ -2,8 +2,6 @@ require "danbooru_image_resizer/danbooru_image_resizer"
require "tmpdir"
class PendingPost < ActiveRecord::Base
class Error < Exception ; end
attr_accessor :file, :image_width, :image_height, :file_ext, :md5, :file_size
belongs_to :uploader, :class_name => "User"
before_save :convert_cgi_file
@@ -26,24 +24,56 @@ class PendingPost < ActiveRecord::Base
update_attribute(:status, "error: " + post.errors.full_messages.join(", "))
end
end
def convert_to_post
returning Post.new do |p|
p.tag_string = tag_string
p.md5 = md5
p.file_ext = file_ext
p.image_width = image_width
p.image_height = image_height
p.uploader_id = uploader_id
p.uploader_ip_addr = uploader_ip_addr
p.updater_id = uploader_id
p.updater_ip_addr = uploader_ip_addr
p.rating = rating
p.source = source
p.file_size = file_size
end
end
def move_file
FileUtils.mv(file_path, md5_file_path)
end
def calculate_file_size(source_path)
self.file_size = File.size(source_path)
end
# Calculates the MD5 based on whatever is in temp_file_path
def calculate_hash(source_path)
self.md5 = Digest::MD5.file(source_path).hexdigest
end
class Error < Exception ; end
# private
module ResizerMethods
def generate_resizes(source_path)
generate_resize_for(Danbooru.config.small_image_width, source_path)
generate_resize_for(Danbooru.config.medium_image_width, source_path)
generate_resize_for(Danbooru.config.large_image_width, source_path)
generate_resize_for(Danbooru.config.small_image_width, Danbooru.config.small_image_width, source_path)
generate_resize_for(Danbooru.config.medium_image_width, nil, source_path)
generate_resize_for(Danbooru.config.large_image_width, nil, source_path)
end
def generate_resize_for(width, source_path)
def generate_resize_for(width, height, source_path)
return if width.nil?
return unless image_width > width
return unless height.nil? || image_height > height
unless File.exists?(source_path)
raise Error.new("file not found")
end
size = Danbooru.reduce_to({:width => image_width, :height => image_height}, {:width => width})
size = Danbooru.reduce_to({:width => image_width, :height => image_height}, {:width => width, :height => height})
# If we're not reducing the resolution, only reencode if the source image larger than
# 200 kilobytes.
@@ -177,33 +207,4 @@ class PendingPost < ActiveRecord::Base
include DownloaderMethods
include FilePathMethods
include CgiFileMethods
# private
def convert_to_post
returning Post.new do |p|
p.tag_string = tag_string
p.md5 = md5
p.file_ext = file_ext
p.image_width = image_width
p.image_height = image_height
p.uploader_id = uploader_id
p.uploader_ip_addr = uploader_ip_addr
p.rating = rating
p.source = source
p.file_size = file_size
end
end
def move_file
FileUtils.mv(file_path, md5_file_path)
end
def calculate_file_size(source_path)
self.file_size = File.size(source_path)
end
# Calculates the MD5 based on whatever is in temp_file_path
def calculate_hash(source_path)
self.md5 = Digest::MD5.file(source_path).hexdigest
end
end

View File

@@ -1,6 +1,475 @@
class Post < ActiveRecord::Base
def file_path
prefix = Rails.env == "test" ? "test." : ""
"#{Rails.root}/public/data/original/#{prefix}#{md5}.#{file_ext}"
attr_accessor :updater_id, :updater_ip_addr, :old_tag_string
belongs_to :updater, :class_name => "User"
belongs_to :uploader, :class_name => "User"
has_one :unapproval
after_destroy :delete_files
after_save :create_version
before_save :merge_old_tags
before_save :normalize_tags
has_many :versions, :class_name => "PostVersion"
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
end
module TagMethods
def tag_array(reload = false)
if @tag_array.nil? || reload
@tag_array = Tag.scan_tags(tag_string)
end
@tag_array
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.
db_tags = Tag.scan_tags(tag_string_was)
new_tags = tag_array()
old_tags = Tag.scan_tags(old_tag_string)
self.tag_string = (db_tags + (new_tags - old_tags) - (old_tags - new_tags)).uniq.join(" ")
end
end
def normalize_tags
normalized_tags = Tag.scan_tags(tag_string)
# normalized_tags = TagAlias.to_aliased(normalized_tags)
# normalized_tags = TagImplication.with_implications(normalized_tags)
normalized_tags = parse_metatags(normalized_tags)
self.tag_string = normalized_tags.uniq.join(" ")
end
def parse_metatags(tags)
tags.map do |tag|
if tag =~ /^(?:pool|rating|fav|user|uploader):(.+)/
case $1
when "pool"
parse_pool_tag($2)
when "rating"
parse_rating_tag($2)
when "fav"
parse_fav_tag($2)
when "uploader"
# ignore
when "user"
# ignore
end
nil
else
tag
end
end.compact
end
def parse_pool_tag(text)
case text
when "new"
pool = Pool.create_anonymous
pool.posts << self
when "recent"
raise NotImplementedError
when /^\d+$/
pool = Pool.find_by_id(text.to_i)
pool.posts << self if pool
else
pool = Pool.find_by_name(text)
pool.posts << self if pool
end
end
def parse_rating_tag(rating)
case rating
when /q/
self.rating = "q"
when /e/
self.rating = "e"
when /s/
self.rating = "s"
end
end
def parse_fav_tag(text)
case text
when "add", "new"
add_favorite(updater_id)
when "remove", "rem", "del"
remove_favorite(updater_id)
end
end
end
module FavoriteMethods
def add_favorite(user_id)
self.fav_string += " fav:#{user_id}"
end
def remove_favorite(user_id)
self.fav_string.gsub!(/user:#{user_id}\b\s*/, " ")
end
end
module SearchMethods
class SearchError < Exception ; end
def add_range_relation(arr, field, arel)
case arr[0]
when :eq
arel.where(["#{field} = ?", arr[1]])
when :gt
arel.where(["#{field} > ?"], arr[1])
when :gte
arel.where(["#{field} >= ?", arr[1]])
when :lt
arel.where(["#{field} < ?", arr[1]])
when :lte
arel.where(["#{field} <= ?", arr[1]])
when :between
arel.where(["#{field} BETWEEN ? AND ?", arr[1], arr[2]])
else
# do nothing
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 build_relation(q, options = {})
unless q.is_a?(Hash)
q = Tag.parse_query(q)
end
relation = where()
add_range_relation(q[:post_id], "posts.id", relation)
add_range_relation(q[:mpixels], "posts.width * posts.height / 1000000.0", relation)
add_range_relation(q[:width], "posts.image_width", relation)
add_range_relation(q[:height], "posts.image_height", relation)
add_range_relation(q[:score], "posts.score", relation)
add_range_relation(q[:filesize], "posts.file_size", relation)
add_range_relation(q[:date], "posts.created_at::date", relation)
add_range_relation(q[:general_tag_count], "posts.tag_count_general", relation)
add_range_relation(q[:artist_tag_count], "posts.tag_count_artist", relation)
add_range_relation(q[:copyright_tag_count], "posts.tag_count_copyright", relation)
add_range_relation(q[:character_tag_count], "posts.tag_count_character", relation)
add_range_relation(q[:tag_count], "posts.tag_count", relation)
if options[:before_id]
relation.where(["posts.id < ?", options[:before_id]])
end
if q[:md5].is_a?(String)
relation.where(["posts.md5 IN (?)", q[:md5].split(/,/)])
end
if q[:status] == "deleted"
relation.where("posts.is_deleted = TRUE")
elsif q[:status] == "pending"
relation.where("posts.is_pending = TRUE")
elsif q[:status] == "flagged"
relation.where("posts.is_flagged = TRUE")
else
relation.where("posts.is_deleted = FALSE")
end
if q[:source].is_a?(String)
relation.where(["posts.source LIKE ? ESCAPE E'\\\\", q[:source]])
end
if q[:subscriptions].is_a?(String)
raise NotImplementedError
q[:subscriptions] =~ /^(.+?):(.+)$/
username = $1 || q[:subscriptions]
subscription_name = $2
user = User.find_by_name(username)
if user
post_ids = TagSubscription.find_post_ids(user.id, subscription_name)
relation.where(["posts.id IN (?)", post_ids])
end
end
tag_query_sql = []
if q[:include].any?
tag_query_sql << "(" + escape_string_for_tsquery(q[:include]).join(" | ") + ")"
end
if q[:related].any?
raise SearchError.new("You cannot search for more than #{Danbooru.config.tag_query_limit} tags at a time") if q[:related].size > Danbooru.config.tag_query_limit
tag_query_sql << "(" + escape_string_for_tsquery(q[:related]).join(" & ") + ")"
end
if q[:exclude].any?
raise SearchError.new("You cannot search for more than #{Danbooru.config.tag_query_limit} tags at a time") if q[:exclude].size > Danbooru.config.tag_query_limit
if q[:related].any? || q[:include].any?
tag_query_sql << "!(" + escape_string_for_tsquery(q[:exclude]).join(" | ") + ")"
else
raise SearchError.new("You cannot search for only excluded tags")
end
end
if tag_query_sql.any?
relation.where("posts.tag_index @@ to_tsquery('danbooru', E'" + tag_query_sql.join(" & ") + "')")
end
if q[:rating] == "q"
relation.where("posts.rating = 'q'")
elsif q[:rating] == "s"
relation.where("posts.rating = 's'")
elsif q[:rating] == "e"
relation.where("posts.rating = 'e'")
end
if q[:rating_negated] == "q"
relation.where("posts.rating <> 'q'")
elsif q[:rating_negated] == "s"
relation.where("posts.rating <> 's'")
elsif q[:rating_negated] == "e"
relation.where("posts.rating <> 'e'")
end
case q[:order]
when "id", "id_asc"
relation.order("posts.id")
when "id_desc"
relation.order("posts.id DESC")
when "score", "score_desc"
relation.order("posts.score DESC, posts.id DESC")
when "score_asc"
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.order("posts.image_width * posts.image_height / 1000000.0 DESC, posts.id DESC")
when "mpixels_asc"
relation.order("posts.image_width * posts.image_height / 1000000.0, posts.id DESC")
when "portrait"
relation.order("1.0 * image_width / GREATEST(1, image_height), posts.id DESC")
when "landscape"
relation.order("1.0 * image_width / GREATEST(1, image_height) DESC, p.id DESC")
when "filesize", "filesize_desc"
relation.order("posts.file_size DESC")
when "filesize_asc"
relation.order("posts.file_size")
else
relation.order("posts.id DESC")
end
if options[:limit]
relation.limit(options[:limit])
end
if options[:offset]
relation.offset(options[:offset])
end
relation
end
def find_by_tags(tags, options)
hash = Tag.parse_query(tags)
build_relation(hash, options)
end
end
module UploaderMethods
def uploader_id=(user_id)
self.uploader_string = "user:#{user_id}"
end
def uploader_id
uploader_string[5, 100].to_i
end
end
include FileMethods
include ImageMethods
include ModerationMethods
include PresenterMethods
include VersionMethods
include TagMethods
include FavoriteMethods
include UploaderMethods
end

View File

@@ -0,0 +1,5 @@
class PostVersion < ActiveRecord::Base
class Error < Exception ; end
validates_presence_of :updater_id, :updater_ip_addr
end

View File

@@ -1,4 +1,7 @@
class Tag < ActiveRecord::Base
attr_accessible :category
after_save :update_category_cache
class CategoryMapping
Danbooru.config.reverse_tag_category_mapping.each do |value, category|
define_method(category.downcase) do
@@ -14,212 +17,256 @@ class Tag < ActiveRecord::Base
Danbooru.config.tag_category_mapping[string.downcase] || 0
end
end
attr_accessible :category
after_save {|rec| Cache.put("tag_type:#{cache_safe_name}", rec.category_name)}
### Category Methods ###
def self.categories
@category_mapping ||= CategoryMapping.new
end
def category_name
Danbooru.config.reverse_tag_category_mapping[category]
end
### Statistics Methods ###
def self.trending
raise NotImplementedError
end
### Name Methods ###
def self.normalize_name(name)
name.downcase.tr(" ", "_").gsub(/\A[-~*]+/, "")
end
def self.find_or_create_by_name(name, options = {})
name = normalize_name(name)
category = categories.general
if name =~ /\A(#{categories.regexp}):(.+)\Z/
category = categories.value_for($1)
name = $2
module ViewCountMethods
def increment_view_count(name)
Cache.incr("tvc:#{Cache.sanitize(name)}")
end
tag = find_by_name(name)
if tag
if category > 0 && !(options[:user] && !options[:user].is_privileged? && tag.post_count > 10)
tag.update_attribute(:category, category)
end
module CategoryMethods
module ClassMethods
def categories
@category_mapping ||= CategoryMapping.new
end
tag
else
returning Tag.new do |tag|
tag.name = name
tag.category = category
tag.save
end
end
end
def cache_safe_name
name.gsub(/[^a-zA-Z0-9_-]/, "_")
end
### Update methods ###
def self.mass_edit(start_tags, result_tags, updater_id, updater_ip_addr)
raise NotImplementedError
Post.find_by_tags(start_tags).each do |p|
start = TagAlias.to_aliased(scan_tags(start_tags))
result = TagAlias.to_aliased(scan_tags(result_tags))
tags = (p.cached_tags.scan(/\S+/) - start + result).join(" ")
p.update_attributes(:updater_user_id => updater_id, :updater_ip_addr => updater_ip_addr, :tags => tags)
end
end
### Parse Methods ###
def self.scan_query(query)
query.to_s.downcase.scan(/\S+/).uniq
end
def self.scan_tags(tags)
tags.to_s.downcase.gsub(/[,;*]/, "_").scan(/\S+/).uniq
end
def self.parse_cast(object, type)
case type
when :integer
object.to_i
when :float
object.to_f
when :date
begin
object.to_date
rescue Exception
nil
end
when :filesize
object =~ /^(\d+(?:\.\d*)?|\d*\.\d+)([kKmM]?)[bB]?$/
size = $1.to_f
unit = $2
conversion_factor = case unit
when /m/i
1024 * 1024
when /k/i
1024
else
1
end
(size * conversion_factor).to_i
end
end
def self.parse_helper(range, type = :integer)
# "1", "0.5", "5.", ".5":
# (-?(\d+(\.\d*)?|\d*\.\d+))
case range
when /^(.+?)\.\.(.+)/
return [:between, parse_cast($1, type), parse_cast($2, type)]
when /^<=(.+)/, /^\.\.(.+)/
return [:lte, parse_cast($1, type)]
when /^<(.+)/
return [:lt, parse_cast($1, type)]
when /^>=(.+)/, /^(.+)\.\.$/
return [:gte, parse_cast($1, type)]
when /^>(.+)/
return [:gt, parse_cast($1, type)]
else
return [:eq, parse_cast(range, type)]
end
end
def self.parse_query(query, options = {})
q = Hash.new {|h, k| h[k] = []}
scan_query(query).each do |token|
if token =~ /^(sub|md5|-rating|rating|width|height|mpixels|score|filesize|source|id|date|order|change|status|tagcount|gentagcount|arttagcount|chartagcount|copytagcount):(.+)$/
if $1 == "sub"
q[:subscriptions] = $2
elsif $1 == "md5"
q[:md5] = $2
elsif $1 == "-rating"
q[:rating_negated] = $2
elsif $1 == "rating"
q[:rating] = $2
elsif $1 == "id"
q[:post_id] = parse_helper($2)
elsif $1 == "width"
q[:width] = parse_helper($2)
elsif $1 == "height"
q[:height] = parse_helper($2)
elsif $1 == "mpixels"
q[:mpixels] = parse_helper($2, :float)
elsif $1 == "score"
q[:score] = parse_helper($2)
elsif $1 == "filesize"
q[:filesize] = parse_helper($2, :filesize)
elsif $1 == "source"
q[:source] = $2.to_escaped_for_sql_like + "%"
elsif $1 == "date"
q[:date] = parse_helper($2, :date)
elsif $1 == "tagcount"
q[:tag_count] = parse_helper($2)
elsif $1 == "gentagcount"
q[:general_tag_count] = parse_helper($2)
elsif $1 == "arttagcount"
q[:artist_tag_count] = parse_helper($2)
elsif $1 == "chartagcount"
q[:character_tag_count] = parse_helper($2)
elsif $1 == "copytagcount"
q[:copyright_tag_count] = parse_helper($2)
elsif $1 == "order"
q[:order] = $2
elsif $1 == "change"
q[:change] = parse_helper($2)
elsif $1 == "status"
q[:status] = $2
def category_for(tag_name)
Cache.get("tc:#{Cache.sanitize(tag_name)}") do
select_value_sql("SELECT category FROM tags WHERE name = ?", tag_name).to_i
end
elsif token[0] == "-" && token.size > 1
q[:exclude] << token[1..-1]
elsif token[0] == "~" && token.size > 1
q[:include] << token[1..-1]
elsif token.include?("*")
matches = where(["name LIKE ? ESCAPE E'\\\\'", token.to_escaped_for_sql_like]).all(:select => "name", :limit => 25, :order => "post_count DESC").map(&:name)
matches = ["~no_matches~"] if matches.empty?
q[:include] += matches
else
q[:related] << token
end
def categories_for(tag_names)
key_hash = tag_names.inject({}) do |hash, x|
hash[x] = "tc:#{Cache.sanitize(x)}"
hash
end
categories_hash = MEMCACHE.get_multi(key_hash.values)
returning({}) do |result_hash|
key_hash.each do |tag_name, hash_key|
if categories_hash.has_key?(hash_key)
result_hash[tag_name] = categories_hash[hash_key]
else
result_hash[tag_name] = category_for(tag_name)
end
end
end
end
end
def self.included(m)
m.extend(ClassMethods)
end
def category_name
Danbooru.config.reverse_tag_category_mapping[category]
end
def update_category_cache
Cache.put("tc:#{Cache.sanitize(name)}", category)
end
end
module StatisticsMethods
def trending
raise NotImplementedError
end
end
module NameMethods
module ClassMethods
def normalize_name(name)
name.downcase.tr(" ", "_").gsub(/\A[-~*]+/, "")
end
def find_or_create_by_name(name, options = {})
name = normalize_name(name)
category = categories.general
if name =~ /\A(#{categories.regexp}):(.+)\Z/
category = categories.value_for($1)
name = $2
end
tag = find_by_name(name)
if tag
if category > 0 && !(options[:user] && !options[:user].is_privileged? && tag.post_count > 10)
tag.update_attribute(:category, category)
end
tag
else
returning Tag.new do |tag|
tag.name = name
tag.category = category
tag.save
end
end
end
end
def self.included(m)
m.extend(ClassMethods)
end
end
module UpdateMethods
def mass_edit(start_tags, result_tags, updater_id, updater_ip_addr)
raise NotImplementedError
Post.find_by_tags(start_tags).each do |p|
start = TagAlias.to_aliased(scan_tags(start_tags))
result = TagAlias.to_aliased(scan_tags(result_tags))
tags = (p.cached_tags.scan(/\S+/) - start + result).join(" ")
p.update_attributes(:updater_user_id => updater_id, :updater_ip_addr => updater_ip_addr, :tags => tags)
end
end
end
module ParseMethods
def scan_query(query)
query.to_s.downcase.scan(/\S+/).uniq
end
def scan_tags(tags)
tags.to_s.downcase.gsub(/[,;*]/, "_").scan(/\S+/).uniq
end
def parse_cast(object, type)
case type
when :integer
object.to_i
when :float
object.to_f
when :date
begin
object.to_date
rescue Exception
nil
end
when :filesize
object =~ /\A(\d+(?:\.\d*)?|\d*\.\d+)([kKmM]?)[bB]?\Z/
size = $1.to_f
unit = $2
conversion_factor = case unit
when /m/i
1024 * 1024
when /k/i
1024
else
1
end
(size * conversion_factor).to_i
end
end
normalize_tags_in_query(q)
def parse_helper(range, type = :integer)
# "1", "0.5", "5.", ".5":
# (-?(\d+(\.\d*)?|\d*\.\d+))
case range
when /\A(.+?)\.\.(.+)/
return [:between, parse_cast($1, type), parse_cast($2, type)]
return q
when /\A<=(.+)/, /\A\.\.(.+)/
return [:lte, parse_cast($1, type)]
when /\A<(.+)/
return [:lt, parse_cast($1, type)]
when /\A>=(.+)/, /\A(.+)\.\.\Z/
return [:gte, parse_cast($1, type)]
when /\A>(.+)/
return [:gt, parse_cast($1, type)]
else
return [:eq, parse_cast(range, type)]
end
end
def parse_query(query, options = {})
q = Hash.new {|h, k| h[k] = []}
scan_query(query).each do |token|
if token =~ /\A(sub|md5|-rating|rating|width|height|mpixels|score|filesize|source|id|date|order|change|status|tagcount|gentagcount|arttagcount|chartagcount|copytagcount):(.+)\Z/
if $1 == "sub"
q[:subscriptions] = $2
elsif $1 == "md5"
q[:md5] = $2
elsif $1 == "-rating"
q[:rating_negated] = $2
elsif $1 == "rating"
q[:rating] = $2
elsif $1 == "id"
q[:post_id] = parse_helper($2)
elsif $1 == "width"
q[:width] = parse_helper($2)
elsif $1 == "height"
q[:height] = parse_helper($2)
elsif $1 == "mpixels"
q[:mpixels] = parse_helper($2, :float)
elsif $1 == "score"
q[:score] = parse_helper($2)
elsif $1 == "filesize"
q[:filesize] = parse_helper($2, :filesize)
elsif $1 == "source"
q[:source] = $2.to_escaped_for_sql_like + "%"
elsif $1 == "date"
q[:date] = parse_helper($2, :date)
elsif $1 == "tagcount"
q[:tag_count] = parse_helper($2)
elsif $1 == "gentagcount"
q[:general_tag_count] = parse_helper($2)
elsif $1 == "arttagcount"
q[:artist_tag_count] = parse_helper($2)
elsif $1 == "chartagcount"
q[:character_tag_count] = parse_helper($2)
elsif $1 == "copytagcount"
q[:copyright_tag_count] = parse_helper($2)
elsif $1 == "order"
q[:order] = $2
elsif $1 == "change"
q[:change] = parse_helper($2)
elsif $1 == "status"
q[:status] = $2
end
elsif token[0] == "-" && token.size > 1
q[:exclude] << token[1..-1]
elsif token[0] == "~" && token.size > 1
q[:include] << token[1..-1]
elsif token.include?("*")
matches = where(["name LIKE ? ESCAPE E'\\\\'", token.to_escaped_for_sql_like]).all(:select => "name", :limit => 25, :order => "post_count DESC").map(&:name)
matches = ["~no_matches~"] if matches.empty?
q[:include] += matches
else
q[:related] << token
end
end
normalize_tags_in_query(q)
return q
end
def normalize_tags_in_query(query_hash)
query_hash[:exclude] = TagAlias.to_aliased(query_hash[:exclude], :strip_prefix => true) if query_hash.has_key?(:exclude)
query_hash[:include] = TagAlias.to_aliased(query_hash[:include], :strip_prefix => true) if query_hash.has_key?(:include)
query_hash[:related] = TagAlias.to_aliased(query_hash[:related]) if query_hash.has_key?(:related)
end
end
def self.normalize_tags_in_query(query_hash)
query_hash[:exclude] = TagAlias.to_aliased(query_hash[:exclude], :strip_prefix => true) if query_hash.has_key?(:exclude)
query_hash[:include] = TagAlias.to_aliased(query_hash[:include], :strip_prefix => true) if query_hash.has_key?(:include)
query_hash[:related] = TagAlias.to_aliased(query_hash[:related]) if query_hash.has_key?(:related)
end
extend ViewCountMethods
include CategoryMethods
extend StatisticsMethods
include NameMethods
extend UpdateMethods
extend ParseMethods
end

6
app/models/unapproval.rb Normal file
View File

@@ -0,0 +1,6 @@
class Unapproval < ActiveRecord::Base
class Error < Exception ; end
belongs_to :unapprover, :class_name => "User"
validates_presence_of :reason, :unapprover_id, :unapprover_ip_addr
end

View File

@@ -2,67 +2,77 @@ require 'digest/sha1'
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :password_hash, :email, :last_logged_in_at, :last_forum_read_at, :has_mail, :receive_email_notifications, :comment_threshold, :always_resize_images, :favorite_tags, :blacklisted_tags
validates_length_of :name, :within => 2..20, :on => :create
validates_format_of :name, :with => /\A[^\s;,]+\Z/, :on => :create, :message => "cannot have whitespace, commas, or semicolons"
validates_uniqueness_of :name, :case_sensitive => false, :on => :create
validates_uniqueness_of :email, :case_sensitive => false, :on => :create, :if => lambda {|rec| !rec.email.blank?}
validates_length_of :password, :minimum => 5, :if => lambda {|rec| rec.password}
validates_inclusion_of :default_image_size, :in => %w(medium large original)
validates_confirmation_of :password
before_save :encrypt_password
after_save {|rec| Cache.put("user_name:#{rec.id}", rec.name)}
scope :named, lambda {|name| where(["lower(name) = ?", name])}
def can_update?(object, foreign_key = :user_id)
is_moderator? || is_admin? || object.__send__(foreign_key) == id
end
### Name Methods ###
def self.find_name(user_id)
Cache.get("user_name:#{user_id}") do
select_value_sql("SELECT name FROM users WHERE id = ?", user_id) || Danbooru.config.default_guest_name
module NameMethods
module ClassMethods
def find_name(user_id)
Cache.get("user_name:#{user_id}", 24.hours) do
select_value_sql("SELECT name FROM users WHERE id = ?", user_id) || Danbooru.config.default_guest_name
end
end
end
end
def pretty_name
name.tr("_", " ")
end
### Password Methods ###
def encrypt_password
self.password_hash = self.class.sha1(password) if password
end
def reset_password
consonants = "bcdfghjklmnpqrstvqxyz"
vowels = "aeiou"
pass = ""
4.times do
pass << consonants[rand(21), 1]
pass << vowels[rand(5), 1]
def self.included(m)
m.extend(ClassMethods)
end
pass << rand(100).to_s
execute_sql("UPDATE users SET password_hash = ? WHERE id = ?", self.class.sha1(pass), id)
pass
def pretty_name
name.tr("_", " ")
end
end
### Authentication Methods ###
def self.authenticate(name, pass)
authenticate_hash(name, sha1(pass))
end
module PasswordMethods
def encrypt_password
self.password_hash = self.class.sha1(password) if password
end
def self.authenticate_hash(name, pass)
where(["lower(name) = ? AND password_hash = ?", name.downcase, pass]).first != nil
end
def reset_password
consonants = "bcdfghjklmnpqrstvqxyz"
vowels = "aeiou"
pass = ""
def self.sha1(pass)
Digest::SHA1.hexdigest("#{Danbooru.config.password_salt}--#{pass}--")
4.times do
pass << consonants[rand(21), 1]
pass << vowels[rand(5), 1]
end
pass << rand(100).to_s
execute_sql("UPDATE users SET password_hash = ? WHERE id = ?", self.class.sha1(pass), id)
pass
end
end
module AuthenticationMethods
def authenticate(name, pass)
authenticate_hash(name, sha1(pass))
end
def authenticate_hash(name, pass)
where(["lower(name) = ? AND password_hash = ?", name.downcase, pass]).first != nil
end
def sha1(pass)
Digest::SHA1.hexdigest("#{Danbooru.config.password_salt}--#{pass}--")
end
end
include NameMethods
include PasswordMethods
extend AuthenticationMethods
end