diff --git a/app/models/pending_post.rb b/app/models/pending_post.rb index f83d8d749..20c17820e 100644 --- a/app/models/pending_post.rb +++ b/app/models/pending_post.rb @@ -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 \ No newline at end of file diff --git a/app/models/post.rb b/app/models/post.rb index dd9631d39..c6960d563 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -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 diff --git a/app/models/post_version.rb b/app/models/post_version.rb new file mode 100644 index 000000000..aec1dd4e4 --- /dev/null +++ b/app/models/post_version.rb @@ -0,0 +1,5 @@ +class PostVersion < ActiveRecord::Base + class Error < Exception ; end + + validates_presence_of :updater_id, :updater_ip_addr +end diff --git a/app/models/tag.rb b/app/models/tag.rb index dcbcc85e7..cde6e1767 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -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 diff --git a/app/models/unapproval.rb b/app/models/unapproval.rb new file mode 100644 index 000000000..0341e6289 --- /dev/null +++ b/app/models/unapproval.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 1c493cd36..146dd8aa4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/db/development_structure.sql b/db/development_structure.sql index 083b8510d..f64c988ca 100644 --- a/db/development_structure.sql +++ b/db/development_structure.sql @@ -113,6 +113,42 @@ CREATE SEQUENCE pending_posts_id_seq ALTER SEQUENCE pending_posts_id_seq OWNED BY pending_posts.id; +-- +-- Name: post_versions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE post_versions ( + id integer NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone, + post_id integer NOT NULL, + source character varying(255), + rating character(1) DEFAULT 'q'::bpchar NOT NULL, + tag_string text NOT NULL, + updater_id integer NOT NULL, + updater_ip_addr inet NOT NULL +); + + +-- +-- Name: post_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE post_versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: post_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE post_versions_id_seq OWNED BY post_versions.id; + + -- -- Name: posts; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -129,10 +165,13 @@ CREATE TABLE posts ( is_rating_locked boolean DEFAULT false NOT NULL, is_pending boolean DEFAULT false NOT NULL, is_flagged boolean DEFAULT false NOT NULL, - approver_id integer, - change_seq integer DEFAULT 0, - uploader_id integer NOT NULL, + is_deleted boolean DEFAULT false NOT NULL, + uploader_string character varying(255) NOT NULL, uploader_ip_addr inet NOT NULL, + approver_string character varying(255) DEFAULT ''::character varying NOT NULL, + fav_string text DEFAULT ''::text NOT NULL, + pool_string text DEFAULT ''::text NOT NULL, + view_count integer DEFAULT 0 NOT NULL, last_noted_at timestamp without time zone, last_commented_at timestamp without time zone, tag_string text NOT NULL, @@ -143,9 +182,9 @@ CREATE TABLE posts ( tag_count_character integer DEFAULT 0 NOT NULL, tag_count_copyright integer DEFAULT 0 NOT NULL, file_ext character varying(255) NOT NULL, + file_size integer NOT NULL, image_width integer NOT NULL, - image_height integer NOT NULL, - file_size integer NOT NULL + image_height integer NOT NULL ); @@ -185,6 +224,7 @@ CREATE TABLE tags ( id integer NOT NULL, name character varying(255) NOT NULL, post_count integer DEFAULT 0 NOT NULL, + view_count integer DEFAULT 0 NOT NULL, category integer DEFAULT 0 NOT NULL, related_tags text, created_at timestamp without time zone, @@ -211,6 +251,40 @@ CREATE SEQUENCE tags_id_seq ALTER SEQUENCE tags_id_seq OWNED BY tags.id; +-- +-- Name: unapprovals; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE unapprovals ( + id integer NOT NULL, + post_id integer NOT NULL, + reason text, + unapprover_id integer NOT NULL, + unapprover_ip_addr inet NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: unapprovals_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE unapprovals_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: unapprovals_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE unapprovals_id_seq OWNED BY unapprovals.id; + + -- -- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -235,6 +309,7 @@ CREATE TABLE users ( receive_email_notifications boolean DEFAULT false NOT NULL, comment_threshold integer DEFAULT (-1) NOT NULL, always_resize_images boolean DEFAULT false NOT NULL, + default_image_size character varying(255) DEFAULT 'medium'::character varying NOT NULL, favorite_tags text, blacklisted_tags text ); @@ -266,6 +341,13 @@ ALTER SEQUENCE users_id_seq OWNED BY users.id; ALTER TABLE pending_posts ALTER COLUMN id SET DEFAULT nextval('pending_posts_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE post_versions ALTER COLUMN id SET DEFAULT nextval('post_versions_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -280,6 +362,13 @@ ALTER TABLE posts ALTER COLUMN id SET DEFAULT nextval('posts_id_seq'::regclass); ALTER TABLE tags ALTER COLUMN id SET DEFAULT nextval('tags_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE unapprovals ALTER COLUMN id SET DEFAULT nextval('unapprovals_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -295,6 +384,14 @@ ALTER TABLE ONLY pending_posts ADD CONSTRAINT pending_posts_pkey PRIMARY KEY (id); +-- +-- Name: post_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY post_versions + ADD CONSTRAINT post_versions_pkey PRIMARY KEY (id); + + -- -- Name: posts_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -311,6 +408,14 @@ ALTER TABLE ONLY tags ADD CONSTRAINT tags_pkey PRIMARY KEY (id); +-- +-- Name: unapprovals_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY unapprovals + ADD CONSTRAINT unapprovals_pkey PRIMARY KEY (id); + + -- -- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -320,17 +425,17 @@ ALTER TABLE ONLY users -- --- Name: index_posts_on_approver_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- Name: index_post_versions_on_post_id; Type: INDEX; Schema: public; Owner: -; Tablespace: -- -CREATE INDEX index_posts_on_approver_id ON posts USING btree (approver_id); +CREATE INDEX index_post_versions_on_post_id ON post_versions USING btree (post_id); -- --- Name: index_posts_on_change_seq; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- Name: index_post_versions_on_updater_id; Type: INDEX; Schema: public; Owner: -; Tablespace: -- -CREATE INDEX index_posts_on_change_seq ON posts USING btree (change_seq); +CREATE INDEX index_post_versions_on_updater_id ON post_versions USING btree (updater_id); -- @@ -404,10 +509,10 @@ CREATE INDEX index_posts_on_tags_index ON posts USING gin (tag_index); -- --- Name: index_posts_on_uploader_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- Name: index_posts_on_view_count; Type: INDEX; Schema: public; Owner: -; Tablespace: -- -CREATE INDEX index_posts_on_uploader_id ON posts USING btree (uploader_id); +CREATE INDEX index_posts_on_view_count ON posts USING btree (view_count); -- @@ -417,6 +522,13 @@ CREATE INDEX index_posts_on_uploader_id ON posts USING btree (uploader_id); CREATE UNIQUE INDEX index_tags_on_name ON tags USING btree (name); +-- +-- Name: index_unapprovals_on_post_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_unapprovals_on_post_id ON unapprovals USING btree (post_id); + + -- -- Name: index_users_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -445,7 +557,7 @@ CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations USING btree (v CREATE TRIGGER trigger_posts_on_tag_index_update BEFORE INSERT OR UPDATE ON posts FOR EACH ROW - EXECUTE PROCEDURE tsvector_update_trigger('tag_index', 'public.danbooru', 'tag_string'); + EXECUTE PROCEDURE tsvector_update_trigger('tag_index', 'public.danbooru', 'tag_string', 'fav_string', 'pool_string', 'uploader_string', 'approver_string'); -- @@ -458,4 +570,8 @@ INSERT INTO schema_migrations (version) VALUES ('20100204214746'); INSERT INTO schema_migrations (version) VALUES ('20100205162521'); -INSERT INTO schema_migrations (version) VALUES ('20100205224030'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20100205163027'); + +INSERT INTO schema_migrations (version) VALUES ('20100205224030'); + +INSERT INTO schema_migrations (version) VALUES ('20100209201251'); \ No newline at end of file diff --git a/db/migrate/20100204211522_create_users.rb b/db/migrate/20100204211522_create_users.rb index 97b070041..48423c12d 100644 --- a/db/migrate/20100204211522_create_users.rb +++ b/db/migrate/20100204211522_create_users.rb @@ -23,6 +23,7 @@ class CreateUsers < ActiveRecord::Migration t.column :receive_email_notifications, :boolean, :null => false, :default => false t.column :comment_threshold, :integer, :null => false, :default => -1 t.column :always_resize_images, :boolean, :null => false, :default => false + t.column :default_image_size, :string, :null => false, :default => "medium" t.column :favorite_tags, :text t.column :blacklisted_tags, :text end diff --git a/db/migrate/20100204214746_create_posts.rb b/db/migrate/20100204214746_create_posts.rb index 06be418f4..fda49c48a 100644 --- a/db/migrate/20100204214746_create_posts.rb +++ b/db/migrate/20100204214746_create_posts.rb @@ -7,25 +7,34 @@ class CreatePosts < ActiveRecord::Migration t.column :source, :string t.column :md5, :string, :null => false t.column :rating, :character, :null => false, :default => 'q' + + # Statuses t.column :is_note_locked, :boolean, :null => false, :default => false t.column :is_rating_locked, :boolean, :null => false, :default => false t.column :is_pending, :boolean, :null => false, :default => false t.column :is_flagged, :boolean, :null => false, :default => false t.column :is_deleted, :boolean, :null => false, :default => false - t.column :approver_id, :integer - t.column :change_seq, :integer, :default => "nextval('post_change_seq'::regclass)" # Uploader - t.column :uploader_id, :integer, :null => false + t.column :uploader_string, :string, :null => false t.column :uploader_ip_addr, "inet", :null => false + + # Approver + t.column :approver_string, :string, :null => false, :default => "" + + # Favorites + t.column :fav_string, :text, :null => false, :default => "" + + # Pools + t.column :pool_string, :text, :null => false, :default => "" # Cached - t.column :fav_count, :integer + t.column :view_count, :integer, :null => false, :default => 0 t.column :last_noted_at, :datetime t.column :last_commented_at, :datetime # Tags - t.column :tag_string, :text, :null => false + t.column :tag_string, :text, :null => false, :default => "" t.column :tag_index, "tsvector" t.column :tag_count, :integer, :null => false, :default => 0 t.column :tag_count_general, :integer, :null => false, :default => 0 @@ -35,22 +44,20 @@ class CreatePosts < ActiveRecord::Migration # File t.column :file_ext, :string, :null => false + t.column :file_size, :integer, :null => false t.column :image_width, :integer, :null => false t.column :image_height, :integer, :null => false - t.column :file_size, :integer, :null => false end add_index :posts, :md5, :unique => true add_index :posts, :created_at add_index :posts, :last_commented_at add_index :posts, :last_noted_at - add_index :posts, :uploader_id - add_index :posts, :approver_id - add_index :posts, :change_seq add_index :posts, :file_size add_index :posts, :image_width add_index :posts, :image_height add_index :posts, :source + add_index :posts, :view_count execute "CREATE INDEX index_posts_on_mpixels ON posts (((image_width * image_height)::numeric / 1000000.0))" @@ -89,7 +96,7 @@ class CreatePosts < ActiveRecord::Migration execute "CREATE TEXT SEARCH CONFIGURATION public.danbooru (PARSER = public.testparser)" execute "ALTER TEXT SEARCH CONFIGURATION public.danbooru ADD MAPPING FOR WORD WITH SIMPLE" execute "SET default_text_search_config = 'public.danbooru'" - execute "CREATE TRIGGER trigger_posts_on_tag_index_update BEFORE INSERT OR UPDATE ON posts FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('tag_index', 'public.danbooru', 'tag_string')" + execute "CREATE TRIGGER trigger_posts_on_tag_index_update BEFORE INSERT OR UPDATE ON posts FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('tag_index', 'public.danbooru', 'tag_string', 'fav_string', 'pool_string', 'uploader_string', 'approver_string')" end def self.down diff --git a/db/migrate/20100205162521_create_tags.rb b/db/migrate/20100205162521_create_tags.rb index 1f54eaf07..5b2f93ded 100644 --- a/db/migrate/20100205162521_create_tags.rb +++ b/db/migrate/20100205162521_create_tags.rb @@ -3,6 +3,7 @@ class CreateTags < ActiveRecord::Migration create_table :tags do |t| t.column :name, :string, :null => false t.column :post_count, :integer, :null => false, :default => 0 + t.column :view_count, :integer, :null => false, :default => 0 t.column :category, :integer, :null => false, :default => 0 t.column :related_tags, :text t.timestamps diff --git a/db/migrate/20100205163027_create_post_versions.rb b/db/migrate/20100205163027_create_post_versions.rb new file mode 100644 index 000000000..377316248 --- /dev/null +++ b/db/migrate/20100205163027_create_post_versions.rb @@ -0,0 +1,26 @@ +class CreatePostVersions < ActiveRecord::Migration + def self.up + create_table :post_versions do |t| + t.timestamps + + # Post + t.column :post_id, :integer, :null => false + + # Versioned + t.column :source, :string + t.column :rating, :character, :null => false, :default => 'q' + t.column :tag_string, :text, :null => false + + # Updater + t.column :updater_id, :integer, :null => false + t.column :updater_ip_addr, "inet", :null => false + end + + add_index :post_versions, :post_id + add_index :post_versions, :updater_id + end + + def self.down + drop_table :post_versions + end +end diff --git a/db/migrate/20100209201251_create_unapprovals.rb b/db/migrate/20100209201251_create_unapprovals.rb new file mode 100644 index 000000000..00462bdc9 --- /dev/null +++ b/db/migrate/20100209201251_create_unapprovals.rb @@ -0,0 +1,17 @@ +class CreateUnapprovals < ActiveRecord::Migration + def self.up + create_table :unapprovals do |t| + t.column :post_id, :integer, :null => false + t.column :reason, :text + t.column :unapprover_id, :integer, :null => false + t.column :unapprover_ip_addr, "inet", :null => false + t.timestamps + end + + add_index :unapprovals, :post_id + end + + def self.down + drop_table :unapprovals + end +end diff --git a/lib/cache.rb b/lib/cache.rb index e2c2a7dcf..654b2ff0f 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -14,16 +14,13 @@ module Cache end end - def incr(key) - val = Cache.get(key) + def incr(key, expiry = 0) + val = Cache.get(key, expiry) Cache.put(key, val.to_i + 1) ActiveRecord::Base.logger.debug('MemCache Incr %s' % [key]) end def get(key, expiry = 0) - key.gsub!(/\s/, "_") - key = key[0, 200] - if block_given? return yield else @@ -79,8 +76,8 @@ module Cache end end - def sanitize_key(key) - key.gsub(/\W/, "_").slice(0, 220) + def sanitize(key) + key.gsub(/\W/) {|x| "%#{x.ord}"}.slice(0, 240) end module_function :get @@ -88,5 +85,5 @@ module Cache module_function :incr module_function :put module_function :delete - module_function :sanitize_key + module_function :sanitize end diff --git a/lib/tasks/db_test_reset.rake b/lib/tasks/db_test_reset.rake new file mode 100644 index 000000000..37fd1cc55 --- /dev/null +++ b/lib/tasks/db_test_reset.rake @@ -0,0 +1,6 @@ +namespace :db do + namespace :test do + task :reset => [:environment, "db:drop", "db:create", "db:migrate", "db:structure:dump", "db:test:clone_structure"] do + end + end +end diff --git a/test/factories/post.rb b/test/factories/post.rb new file mode 100644 index 000000000..cb48eda4e --- /dev/null +++ b/test/factories/post.rb @@ -0,0 +1,12 @@ +Factory.define(:post) do |f| + f.md5 "abcd" + f.uploader {|x| x.association(:user)} + f.uploader_ip_addr "127.0.0.1" + f.tag_string "tag1 tag2" + f.tag_count 2 + f.tag_count_general 2 + f.file_ext "jpg" + f.image_width 100 + f.image_height 200 + f.file_size 2000 +end diff --git a/test/factories/tag.rb b/test/factories/tag.rb index 9877bf2b2..7a1b4a0c0 100644 --- a/test/factories/tag.rb +++ b/test/factories/tag.rb @@ -8,3 +8,11 @@ end Factory.define(:artist_tag, :parent => :tag) do |f| f.category Tag.categories.artist end + +Factory.define(:copyright_tag, :parent => :tag) do |f| + f.category Tag.categories.copyright +end + +Factory.define(:character_tag, :parent => :tag) do |f| + f.category Tag.categories.character +end diff --git a/test/factories/user.rb b/test/factories/user.rb index 3151312a8..6459bb8b0 100644 --- a/test/factories/user.rb +++ b/test/factories/user.rb @@ -2,6 +2,7 @@ Factory.define(:user) do |f| f.name {Faker::Name.first_name} f.password_hash {User.sha1("password")} f.email {Faker::Internet.email} + f.default_image_size "medium" end Factory.define(:banned_user, :parent => :user) do |f| diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb index b327db03e..c7bb77faa 100644 --- a/test/unit/tag_test.rb +++ b/test/unit/tag_test.rb @@ -1,6 +1,31 @@ require File.dirname(__FILE__) + '/../test_helper' class TagTest < ActiveSupport::TestCase + context "A tag category fetcher" do + setup do + MEMCACHE.flush_all + end + + should "fetch for a single tag" do + Factory.create(:artist_tag, :name => "test") + assert_equal(Tag.categories.artist, Tag.category_for("test")) + end + + should "fetch for a single tag with strange markup" do + Factory.create(:artist_tag, :name => "!@$%") + assert_equal(Tag.categories.artist, Tag.category_for("!@$%")) + end + + should "fetch for multiple tags" do + Factory.create(:artist_tag, :name => "aaa") + Factory.create(:copyright_tag, :name => "bbb") + categories = Tag.categories_for(%w(aaa bbb ccc)) + assert_equal(Tag.categories.artist, categories["aaa"]) + assert_equal(Tag.categories.copyright, categories["bbb"]) + assert_equal(0, categories["ccc"]) + end + end + context "A tag category mapping" do setup do MEMCACHE.flush_all @@ -51,17 +76,12 @@ class TagTest < ActiveSupport::TestCase assert_equal("Artist", @tag.category_name) end - should "know its cache safe name" do - tag = Tag.new - - tag.name = "tag" - assert_equal("tag", tag.cache_safe_name) - - tag.name = "tag%" - assert_equal("tag_", tag.cache_safe_name) - - tag.name = "tag%%" - assert_equal("tag__", tag.cache_safe_name) + should "reset its category after updating" do + tag = Factory.create(:artist_tag) + assert_equal(Tag.categories.artist, MEMCACHE.get("tc:#{tag.name}")) + + tag.update_attribute(:category, Tag.categories.copyright) + assert_equal(Tag.categories.copyright, MEMCACHE.get("tc:#{tag.name}")) end end diff --git a/test/unit/unapproval_test.rb b/test/unit/unapproval_test.rb new file mode 100644 index 000000000..de18601b9 --- /dev/null +++ b/test/unit/unapproval_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class PostUnapprovalTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end