tag unit test
This commit is contained in:
@@ -1,2 +1,224 @@
|
|||||||
class Tag < ActiveRecord::Base
|
class Tag < ActiveRecord::Base
|
||||||
|
class CategoryMapping
|
||||||
|
Danbooru.config.reverse_tag_category_mapping.each do |value, category|
|
||||||
|
define_method(category.downcase) do
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def regexp
|
||||||
|
@regexp ||= Regexp.compile(Danbooru.config.tag_category_mapping.keys.sort_by {|x| -x.size}.join("|"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_for(string)
|
||||||
|
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 = self.class.types.general
|
||||||
|
|
||||||
|
if name =~ /\A(#{categories.regexp}):(.+)\Z/
|
||||||
|
category = self.class.types.value_for($1)
|
||||||
|
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
|
||||||
|
|
||||||
|
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 == "user"
|
||||||
|
q[:user] = $2
|
||||||
|
elsif $1 == "fav"
|
||||||
|
q[:fav] = $2
|
||||||
|
elsif $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
|
||||||
|
|
||||||
|
q[:exclude] = TagAlias.to_aliased(q[:exclude], :strip_prefix => true) if q.has_key?(:exclude)
|
||||||
|
q[:include] = TagAlias.to_aliased(q[:include], :strip_prefix => true) if q.has_key?(:include)
|
||||||
|
q[:related] = TagAlias.to_aliased(q[:related]) if q.has_key?(:related)
|
||||||
|
|
||||||
|
return q
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ require 'digest/sha1'
|
|||||||
class User < ActiveRecord::Base
|
class User < ActiveRecord::Base
|
||||||
attr_accessor :password
|
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_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_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 :name, :case_sensitive => false, :on => :create
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ module Danbooru
|
|||||||
1024
|
1024
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# When calculating statistics based on the posts table, gather this many posts to sample from.
|
||||||
|
def post_sample_size
|
||||||
|
300
|
||||||
|
end
|
||||||
|
|
||||||
# List of memcached servers
|
# List of memcached servers
|
||||||
def memcached_servers
|
def memcached_servers
|
||||||
%w(localhost:11211)
|
%w(localhost:11211)
|
||||||
@@ -102,8 +107,40 @@ module Danbooru
|
|||||||
5
|
5
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The name of the server the app is hosted on.
|
||||||
def server_host
|
def server_host
|
||||||
Socket.gethostname
|
Socket.gethostname
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns a hash mapping various tag categories to a numerical value.
|
||||||
|
# Be sure to update the reverse_tag_category_mapping also.
|
||||||
|
def tag_category_mapping
|
||||||
|
@tag_category_mapping ||= {
|
||||||
|
"general" => 0,
|
||||||
|
"gen" => 0,
|
||||||
|
|
||||||
|
"artist" => 1,
|
||||||
|
"art" => 1,
|
||||||
|
|
||||||
|
"copyright" => 3,
|
||||||
|
"copy" => 3,
|
||||||
|
"co" => 3,
|
||||||
|
|
||||||
|
"character" => 4,
|
||||||
|
"char" => 4,
|
||||||
|
"ch" => 4
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash maping numerical category values to their
|
||||||
|
# string equivalent. Be sure to update the tag_category_mapping also.
|
||||||
|
def reverse_tag_category_mapping
|
||||||
|
@reverse_tag_category_mapping ||= {
|
||||||
|
0 => "General",
|
||||||
|
1 => "Artist",
|
||||||
|
3 => "Copyright",
|
||||||
|
4 => "Character"
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,6 +24,40 @@ CREATE TABLE schema_migrations (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tags; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE tags (
|
||||||
|
id integer NOT NULL,
|
||||||
|
name character varying(255) NOT NULL,
|
||||||
|
post_count integer DEFAULT 0 NOT NULL,
|
||||||
|
category integer DEFAULT 0 NOT NULL,
|
||||||
|
related_tags text,
|
||||||
|
created_at timestamp without time zone,
|
||||||
|
updated_at timestamp without time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tags_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE tags_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MAXVALUE
|
||||||
|
NO MINVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE tags_id_seq OWNED BY tags.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
-- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
||||||
--
|
--
|
||||||
@@ -72,6 +106,13 @@ CREATE SEQUENCE users_id_seq
|
|||||||
ALTER SEQUENCE users_id_seq OWNED BY users.id;
|
ALTER SEQUENCE users_id_seq OWNED BY users.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE tags ALTER COLUMN id SET DEFAULT nextval('tags_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -79,6 +120,14 @@ ALTER SEQUENCE users_id_seq OWNED BY users.id;
|
|||||||
ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass);
|
ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: tags_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY tags
|
||||||
|
ADD CONSTRAINT tags_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
-- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
||||||
--
|
--
|
||||||
@@ -87,6 +136,13 @@ ALTER TABLE ONLY users
|
|||||||
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_tags_on_name; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX index_tags_on_name ON tags USING btree (name);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: index_users_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
-- Name: index_users_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
||||||
--
|
--
|
||||||
@@ -112,4 +168,6 @@ CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations USING btree (v
|
|||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO schema_migrations (version) VALUES ('20100204211522');
|
INSERT INTO schema_migrations (version) VALUES ('20100204211522');
|
||||||
|
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('20100205162521');
|
||||||
17
db/migrate/20100205162521_create_tags.rb
Normal file
17
db/migrate/20100205162521_create_tags.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class CreateTags < ActiveRecord::Migration
|
||||||
|
def self.up
|
||||||
|
create_table :tags do |t|
|
||||||
|
t.column :name, :string, :null => false
|
||||||
|
t.column :post_count, :integer, :null => false, :default => 0
|
||||||
|
t.column :category, :integer, :null => false, :default => 0
|
||||||
|
t.column :related_tags, :text
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :tags, :name, :unique => true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
drop_table :tags
|
||||||
|
end
|
||||||
|
end
|
||||||
10
test/factories/tag.rb
Normal file
10
test/factories/tag.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Factory.define(:tag) do |f|
|
||||||
|
f.name {Faker::Name.first_name}
|
||||||
|
f.post_count 0
|
||||||
|
f.category Tag.categories.general
|
||||||
|
f.related_tags ""
|
||||||
|
end
|
||||||
|
|
||||||
|
Factory.define(:artist_tag, :parent => :tag) do |f|
|
||||||
|
f.category Tag.categories.artist
|
||||||
|
end
|
||||||
@@ -4,26 +4,26 @@ Factory.define(:user) do |f|
|
|||||||
f.email {Faker::Internet.email}
|
f.email {Faker::Internet.email}
|
||||||
end
|
end
|
||||||
|
|
||||||
Factory.define(:banned_user) do |f|
|
Factory.define(:banned_user, :parent => :user) do |f|
|
||||||
f.is_banned true
|
f.is_banned true
|
||||||
end
|
end
|
||||||
|
|
||||||
Factory.define(:privileged_user) do |f|
|
Factory.define(:privileged_user, :parent => :user) do |f|
|
||||||
f.is_privileged true
|
f.is_privileged true
|
||||||
end
|
end
|
||||||
|
|
||||||
Factory.define(:contributor_user) do |f|
|
Factory.define(:contributor_user, :parent => :user) do |f|
|
||||||
f.is_contributor true
|
f.is_contributor true
|
||||||
end
|
end
|
||||||
|
|
||||||
Factory.define(:janitor_user) do |f|
|
Factory.define(:janitor_user, :parent => :user) do |f|
|
||||||
f.is_janitor true
|
f.is_janitor true
|
||||||
end
|
end
|
||||||
|
|
||||||
Factory.define(:moderator_user) do |f|
|
Factory.define(:moderator_user, :parent => :user) do |f|
|
||||||
f.is_moderator true
|
f.is_moderator true
|
||||||
end
|
end
|
||||||
|
|
||||||
Factory.define(:admin_user) do |f|
|
Factory.define(:admin_user, :parent => :user) do |f|
|
||||||
f.is_admin true
|
f.is_admin true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,54 @@
|
|||||||
require 'test_helper'
|
require File.dirname(__FILE__) + '/../test_helper'
|
||||||
|
|
||||||
class TagTest < ActiveSupport::TestCase
|
class TagTest < ActiveSupport::TestCase
|
||||||
# Replace this with your real tests.
|
context "A tag category mapping" do
|
||||||
test "the truth" do
|
setup do
|
||||||
assert true
|
MEMCACHE.flush_all
|
||||||
|
end
|
||||||
|
|
||||||
|
should "exist" do
|
||||||
|
assert_nothing_raised {Tag.categories}
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have convenience methods for the four main categories" do
|
||||||
|
assert_equal(0, Tag.categories.general)
|
||||||
|
assert_equal(1, Tag.categories.artist)
|
||||||
|
assert_equal(3, Tag.categories.copyright)
|
||||||
|
assert_equal(4, Tag.categories.character)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "have a regular expression for matching category names and shortcuts" do
|
||||||
|
regexp = Tag.categories.regexp
|
||||||
|
|
||||||
|
assert_match(regexp, "artist")
|
||||||
|
assert_match(regexp, "art")
|
||||||
|
assert_match(regexp, "copyright")
|
||||||
|
assert_match(regexp, "copy")
|
||||||
|
assert_match(regexp, "co")
|
||||||
|
assert_match(regexp, "character")
|
||||||
|
assert_match(regexp, "char")
|
||||||
|
assert_match(regexp, "ch")
|
||||||
|
assert_no_match(regexp, "c")
|
||||||
|
assert_no_match(regexp, "woodle")
|
||||||
|
end
|
||||||
|
|
||||||
|
should "map a category name to its value" do
|
||||||
|
assert_equal(0, Tag.categories.value_for("general"))
|
||||||
|
assert_equal(0, Tag.categories.value_for("gen"))
|
||||||
|
assert_equal(1, Tag.categories.value_for("artist"))
|
||||||
|
assert_equal(1, Tag.categories.value_for("art"))
|
||||||
|
assert_equal(0, Tag.categories.value_for("unknown"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "A tag" do
|
||||||
|
setup do
|
||||||
|
MEMCACHE.flush_all
|
||||||
|
end
|
||||||
|
|
||||||
|
should "know its category name" do
|
||||||
|
@tag = Factory.create(:artist_tag)
|
||||||
|
assert_equal("Artist", @tag.category_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user