diff --git a/app/models/favorite.rb b/app/logical/favorite.rb similarity index 58% rename from app/models/favorite.rb rename to app/logical/favorite.rb index a86a54a8a..5f992fe66 100644 --- a/app/models/favorite.rb +++ b/app/logical/favorite.rb @@ -10,4 +10,12 @@ class Favorite def self.destroy(user, post) ActiveRecord::Base.connection.execute("DELETE FROM #{table_name_for(user)} WHERE user_id = #{user.id} AND post_id = #{post.id}") end + + def self.destroy_all_for_post(post) + ActiveRecord::Base.connection.execute("DELETE FROM #{table_name_for(user)} WHERE post_id = #{post.id}") + end + + def self.destroy_all_for_user(user) + ActiveRecord::Base.connection.execute("DELETE FROM #{table_name_for(user)} WHERE user_id = #{user.id}") + end end diff --git a/app/models/artist.rb b/app/models/artist.rb new file mode 100644 index 000000000..193b6044e --- /dev/null +++ b/app/models/artist.rb @@ -0,0 +1,155 @@ +class Artist < ActiveRecord::Base + attr_accessor :updater_id, :updater_ip_addr + before_save :normalize_name + after_save :create_version + after_save :commit_url_string + validates_uniqueness_of :name + belongs_to :updater, :class_name => "User" + belongs_to :creator, :class_name => "User" + has_many :members, :class_name => "Artist", :foreign_key => "group_name", :primary_key => "name" + has_many :artist_urls, :dependent => :destroy + has_one :wiki_page, :foreign_key => "title", :primary_key => "name" + has_one :tag_alias, :foreign_key => "antecedent_name", :primary_key => "name" + accepts_nested_attributes_for :wiki_page + + module UrlMethods + module ClassMethods + def find_all_by_url(url) + url = ArtistUrl.normalize(url) + artists = [] + + while artists.empty? && url.size > 10 + u = url.sub(/\/+$/, "") + "/" + u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%' + artists += Artist.joins(:artist_urls).where(["artists.is_active = TRUE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).all(:order => "artists.name") + url = File.dirname(url) + "/" + end + + artists.uniq_by {|x| x.name}.slice(0, 20) + end + end + + def self.included(m) + m.extend(ClassMethods) + end + + def commit_url_string + if @url_string + artist_urls.clear + + @url_string.scan(/\S+/).each do |url| + artist_urls.create(:url => url) + end + end + end + + def url_string=(string) + @url_string = string + end + + def url_string + @url_string || artist_urls.map {|x| x.url}.join("\n") + end + end + + module NameMethods + module ClassMethods + def normalize_name(name) + name.downcase.strip.gsub(/ /, '_') + end + end + + def self.included(m) + m.extend(ClassMethods) + end + + def normalize_name + self.name = Artist.normalize_name(name) + if other_names + self.other_names = other_names.split(/,/).map {|x| Artist.normalize_name(x)}.join(" ") + end + end + end + + module GroupMethods + def member_names + members.map(&:name).join(", ") + end + end + + module UpdaterMethods + def updater_name + User.find_name(updater_id).tr("_", " ") + end + end + + module SearchMethods + def find_by_any_name(name) + build_relation(:name => name).first + end + + def build_relation(params) + relation = Artist.where("is_active = TRUE") + + case params[:name] + when /^http/ + relation = relation.where("id IN (?)", find_all_by_url(params[:name]).map(&:id)) + + when /name:(.+)/ + escaped_name = Artist.normalize_name($1).to_escaped_for_sql_like + relation = relation.where(["name LIKE ? ESCAPE E'\\\\'", escaped_name]) + + when /other:(.+)/ + escaped_name = Artist.normalize_name($1) + relation = relation.where(["other_names_index @@ to_tsquery('danbooru', ?)", escaped_name]) + + when /group:(.+)/ + escaped_name = Artist.normalize_name($1).to_escaped_for_sql_like + relation = relation.where(["group_name LIKE ? ESCAPE E'\\\\'", escaped_name]) + + when /./ + normalized_name = Artist.normalize_name($1) + escaped_name = normalized_name.to_escaped_for_sql_like + relation = relation.where(["name LIKE ? ESCAPE E'\\\\' OR other_names_index @@ to_tsquery('danbooru', ?) OR group_name LIKE ? ESCAPE E'\\\\'", escaped_name, normalized_name, escaped_name]) + end + + if params[:id] + relation = relation.where(["id = ?", params[:id]]) + end + + relation + end + end + + module VersionMethods + def create_version + ArtistVersion.create( + :artist_id => id, + :name => name, + :updater_id => updater_id, + :updater_ip_addr => updater_ip_addr, + :url_string => url_string, + :is_active => is_active, + :other_names => other_names, + :group_name => group_name + ) + end + + def revert_to!(version) + self.name = version.name + self.url_string = version.url_string + self.is_active = version.is_active + self.other_names = version.other_names + self.group_name = version.group_name + save + end + end + + include UrlMethods + include NameMethods + include GroupMethods + include UpdaterMethods + extend SearchMethods + include VersionMethods +end + diff --git a/app/models/artist_url.rb b/app/models/artist_url.rb new file mode 100644 index 000000000..74d245f9d --- /dev/null +++ b/app/models/artist_url.rb @@ -0,0 +1,37 @@ +class ArtistUrl < ActiveRecord::Base + before_save :normalize + validates_presence_of :url + belongs_to :artist + + def self.normalize(url) + if url.nil? + nil + else + url.gsub!(/^http:\/\/blog\d+\.fc2/, "http://blog.fc2") + url.gsub!(/^http:\/\/blog-imgs-\d+\.fc2/, "http://blog.fc2") + url.gsub!(/^http:\/\/blog-imgs-\d+-\w+\.fc2/, "http://blog.fc2") + url.gsub!(/^http:\/\/img\d+\.pixiv\.net/, "http://img.pixiv.net") + url.gsub!(/\/+$/, "") + url + "/" + end + end + + def self.normalize_for_search(url) + if url =~ /\.\w+$/ && url =~ /\w\/\w/ + url = File.dirname(url) + end + + url = url.gsub(/^http:\/\/blog\d+\.fc2/, "http://blog*.fc2") + url = url.gsub(/^http:\/\/blog-imgs-\d+\.fc2/, "http://blog*.fc2") + url = url.gsub(/^http:\/\/blog-imgs-\d+-\w+\.fc2/, "http://blog*.fc2") + url = url.gsub(/^http:\/\/img\d+\.pixiv\.net/, "http://img*.pixiv.net") + end + + def normalize + self.normalized_url = self.class.normalize(url) + end + + def to_s + url + end +end diff --git a/app/models/artist_version.rb b/app/models/artist_version.rb new file mode 100644 index 000000000..756bb2cde --- /dev/null +++ b/app/models/artist_version.rb @@ -0,0 +1,8 @@ +class ArtistVersion < ActiveRecord::Base + belongs_to :updater + belongs_to :artist + + def updater_name + User.find_name(updater_id).tr("_", " ") + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 000000000..1ec6ee165 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,41 @@ +class Comment < ActiveRecord::Base + class VotingError < Exception ; end + + validates_format_of :body, :with => /\S/, :message => 'has no content' + belongs_to :post + belongs_to :creator, :class_name => "User" + has_many :votes, :class_name => "CommentVote", :dependent => :destroy + after_save :update_last_commented_at + after_destroy :update_last_commented_at + attr_accessible :body + attr_accessor :do_not_bump_post + + scope :recent, :order => "comments.id desc", :limit => 6 + scope :search_body, lambda {|query| {:conditions => ["body_index @@ plainto_tsquery(?)", query], :order => "id desc"}} + + def update_last_commented_at + return if do_not_bump_post + comment_count = Comment.where(["post_id = ?", post_id]).count + if comment_count <= Danbooru.config.comment_threshold + execute_sql("UPDATE posts SET last_commented_at = ? WHERE id = ?", created_at, post_id) + end + end + + def can_be_voted_by?(user) + !votes.exists?(["user_id = ?", user.id]) + end + + def vote!(user, is_positive) + if can_be_voted_by?(user) + if is_positive + increment!(:score) + else + decrement!(:score) + end + + votes.create(:user_id => user.id) + else + raise VotingError.new("You have already voted for this comment") + end + end +end diff --git a/app/models/comment_vote.rb b/app/models/comment_vote.rb new file mode 100644 index 000000000..b9197a470 --- /dev/null +++ b/app/models/comment_vote.rb @@ -0,0 +1,8 @@ +class CommentVote < ActiveRecord::Base + belongs_to :comment + belongs_to :user + + def self.prune! + destroy_all(["created_at < ?", 14.days.ago]) + end +end diff --git a/app/models/pool.rb b/app/models/pool.rb index 8db4049a0..58155b9f2 100644 --- a/app/models/pool.rb +++ b/app/models/pool.rb @@ -5,7 +5,7 @@ class Pool < ActiveRecord::Base validates_format_of :name, :with => /\A[^\s;,]+\Z/, :on => :create, :message => "cannot have whitespace, commas, or semicolons" belongs_to :creator, :class_name => "User" belongs_to :updater, :class_name => "User" - has_many :versions, :class_name => "PoolVersion" + has_many :versions, :class_name => "PoolVersion", :dependent => :destroy after_save :create_version def self.create_anonymous(creator, creator_ip_addr) diff --git a/app/models/post.rb b/app/models/post.rb index 18be9c649..a8fd3aed5 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1,8 +1,7 @@ class Post < ActiveRecord::Base attr_accessor :updater_id, :updater_ip_addr, :old_tag_string - belongs_to :updater, :class_name => "User" - has_one :unapproval after_destroy :delete_files + after_destroy :delete_favorites after_save :create_version before_save :merge_old_tags @@ -11,7 +10,10 @@ class Post < ActiveRecord::Base before_save :update_tag_post_counts before_save :set_tag_counts - has_many :versions, :class_name => "PostVersion" + belongs_to :updater, :class_name => "User" + has_one :unapproval, :dependent => :destroy + has_one :upload, :dependent => :destroy + has_many :versions, :class_name => "PostVersion", :dependent => :destroy module FileMethods def delete_files @@ -225,6 +227,10 @@ class Post < ActiveRecord::Base end module FavoriteMethods + def delete_favorites + Favorite.destroy_all_for_post(self) + end + def add_favorite(user) self.fav_string += " fav:#{user.name}" self.fav_string.strip! diff --git a/app/models/tag.rb b/app/models/tag.rb index a34b40f5e..5751cc7fe 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,6 +1,7 @@ class Tag < ActiveRecord::Base attr_accessible :category after_save :update_category_cache + has_one :wiki_page, :foreign_key => "name", :primary_key => "title" scope :by_pattern, lambda {|name| where(["name LIKE ? ESCAPE E'\\\\'", name.to_escaped_for_sql_like])} class CategoryMapping diff --git a/app/models/upload.rb b/app/models/upload.rb index cdf5e3e7d..b932b8a10 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -4,6 +4,7 @@ require "tmpdir" class Upload < ActiveRecord::Base attr_accessor :file, :image_width, :image_height, :file_ext, :md5, :file_size belongs_to :uploader, :class_name => "User" + belongs_to :post before_save :convert_cgi_file def process! diff --git a/app/models/user.rb b/app/models/user.rb index 0b1c0cbe4..38294fdc7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,7 @@ class User < ActiveRecord::Base 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)} + after_save :update_cache scope :named, lambda {|name| where(["lower(name) = ?", name])} def can_update?(object, foreign_key = :user_id) @@ -21,7 +21,7 @@ class User < ActiveRecord::Base module NameMethods module ClassMethods def find_name(user_id) - Cache.get("user_name:#{user_id}", 24.hours) do + Cache.get("un:#{user_id}") do select_value_sql("SELECT name FROM users WHERE id = ?", user_id) || Danbooru.config.default_guest_name end end @@ -34,6 +34,10 @@ class User < ActiveRecord::Base def pretty_name name.tr("_", " ") end + + def update_cache + Cache.put("un:#{id}", name) + end end module PasswordMethods diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb new file mode 100644 index 000000000..e48fc246c --- /dev/null +++ b/app/models/wiki_page.rb @@ -0,0 +1,3 @@ +class WikiPage < ActiveRecord::Base + attr_accessor :updater_id, :updater_ip_addr +end diff --git a/db/development_structure.sql b/db/development_structure.sql index 218e63177..0a731eeab 100644 --- a/db/development_structure.sql +++ b/db/development_structure.sql @@ -74,6 +74,181 @@ SET default_tablespace = ''; SET default_with_oids = false; +-- +-- Name: artist_urls; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE artist_urls ( + id integer NOT NULL, + artist_id integer NOT NULL, + url text NOT NULL, + normalized_url text NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: artist_urls_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE artist_urls_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: artist_urls_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE artist_urls_id_seq OWNED BY artist_urls.id; + + +-- +-- Name: artist_versions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE artist_versions ( + id integer NOT NULL, + artist_id integer NOT NULL, + name character varying(255) NOT NULL, + updater_id integer NOT NULL, + updater_ip_addr inet NOT NULL, + is_active boolean DEFAULT true NOT NULL, + other_names text, + group_name character varying(255), + url_string text, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: artist_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE artist_versions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: artist_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE artist_versions_id_seq OWNED BY artist_versions.id; + + +-- +-- Name: artists; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE artists ( + id integer NOT NULL, + name character varying(255) NOT NULL, + creator_id integer NOT NULL, + is_active boolean DEFAULT true NOT NULL, + other_names text, + other_names_index tsvector, + group_name character varying(255), + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: artists_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE artists_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: artists_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE artists_id_seq OWNED BY artists.id; + + +-- +-- Name: comment_votes; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE comment_votes ( + id integer NOT NULL, + comment_id integer NOT NULL, + user_id integer NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: comment_votes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE comment_votes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: comment_votes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE comment_votes_id_seq OWNED BY comment_votes.id; + + +-- +-- Name: comments; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE comments ( + id integer NOT NULL, + post_id integer NOT NULL, + creator_id integer NOT NULL, + body text NOT NULL, + ip_addr inet NOT NULL, + body_index tsvector NOT NULL, + score integer DEFAULT 0 NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE comments_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE comments_id_seq OWNED BY comments.id; + + -- -- Name: favorites_0; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -773,6 +948,76 @@ CREATE SEQUENCE users_id_seq ALTER SEQUENCE users_id_seq OWNED BY users.id; +-- +-- Name: wiki_pages; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE wiki_pages ( + id integer NOT NULL, + creator_id integer NOT NULL, + title character varying(255) NOT NULL, + body text NOT NULL, + body_index tsvector NOT NULL, + is_locked boolean DEFAULT false NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: wiki_pages_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE wiki_pages_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: wiki_pages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE wiki_pages_id_seq OWNED BY wiki_pages.id; + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE artist_urls ALTER COLUMN id SET DEFAULT nextval('artist_urls_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE artist_versions ALTER COLUMN id SET DEFAULT nextval('artist_versions_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE artists ALTER COLUMN id SET DEFAULT nextval('artists_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE comment_votes ALTER COLUMN id SET DEFAULT nextval('comment_votes_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE comments ALTER COLUMN id SET DEFAULT nextval('comments_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -913,6 +1158,53 @@ ALTER TABLE uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::regcla ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE wiki_pages ALTER COLUMN id SET DEFAULT nextval('wiki_pages_id_seq'::regclass); + + +-- +-- Name: artist_urls_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY artist_urls + ADD CONSTRAINT artist_urls_pkey PRIMARY KEY (id); + + +-- +-- Name: artist_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY artist_versions + ADD CONSTRAINT artist_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: artists_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY artists + ADD CONSTRAINT artists_pkey PRIMARY KEY (id); + + +-- +-- Name: comment_votes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY comment_votes + ADD CONSTRAINT comment_votes_pkey PRIMARY KEY (id); + + +-- +-- Name: comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY comments + ADD CONSTRAINT comments_pkey PRIMARY KEY (id); + + -- -- Name: favorites_0_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1073,6 +1365,91 @@ ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id); +-- +-- Name: wiki_pages_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY wiki_pages + ADD CONSTRAINT wiki_pages_pkey PRIMARY KEY (id); + + +-- +-- Name: index_artist_urls_on_artist_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artist_urls_on_artist_id ON artist_urls USING btree (artist_id); + + +-- +-- Name: index_artist_urls_on_normalized_url; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artist_urls_on_normalized_url ON artist_urls USING btree (normalized_url); + + +-- +-- Name: index_artist_versions_on_artist_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artist_versions_on_artist_id ON artist_versions USING btree (artist_id); + + +-- +-- Name: index_artist_versions_on_name; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artist_versions_on_name ON artist_versions USING btree (name); + + +-- +-- Name: index_artist_versions_on_updater_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artist_versions_on_updater_id ON artist_versions USING btree (updater_id); + + +-- +-- Name: index_artists_on_group_name; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artists_on_group_name ON artists USING btree (group_name); + + +-- +-- Name: index_artists_on_name; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_artists_on_name ON artists USING btree (name); + + +-- +-- Name: index_artists_on_other_names_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_artists_on_other_names_index ON artists USING gin (other_names_index); + + +-- +-- Name: index_comment_votes_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_comment_votes_on_user_id ON comment_votes USING btree (user_id); + + +-- +-- Name: index_comments_on_body_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_comments_on_body_index ON comments USING gin (body_index); + + +-- +-- Name: index_comments_on_post_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_comments_on_post_id ON comments USING btree (post_id); + + -- -- Name: index_favorites_0_on_post_id; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -1332,6 +1709,13 @@ CREATE INDEX index_posts_on_view_count ON posts USING btree (view_count); CREATE INDEX index_tag_aliases_on_antecedent_name ON tag_aliases USING btree (antecedent_name); +-- +-- Name: index_tag_aliases_on_consequent_name; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_tag_aliases_on_consequent_name ON tag_aliases USING btree (consequent_name); + + -- -- Name: index_tag_implications_on_antecedent_name; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -1339,6 +1723,13 @@ CREATE INDEX index_tag_aliases_on_antecedent_name ON tag_aliases USING btree (an CREATE INDEX index_tag_implications_on_antecedent_name ON tag_implications USING btree (antecedent_name); +-- +-- Name: index_tag_implications_on_consequent_name; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_tag_implications_on_consequent_name ON tag_implications USING btree (consequent_name); + + -- -- Name: index_tags_on_name; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -1367,6 +1758,20 @@ CREATE UNIQUE INDEX index_users_on_email ON users USING btree (email); CREATE UNIQUE INDEX index_users_on_name ON users USING btree (lower((name)::text)); +-- +-- Name: index_wiki_pages_on_body_index_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_wiki_pages_on_body_index_index ON wiki_pages USING gin (body_index); + + +-- +-- Name: index_wiki_pages_on_title; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX index_wiki_pages_on_title ON wiki_pages USING btree (title); + + -- -- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -1374,6 +1779,26 @@ CREATE UNIQUE INDEX index_users_on_name ON users USING btree (lower((name)::text CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations USING btree (version); +-- +-- Name: trigger_artists_on_update; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trigger_artists_on_update + BEFORE INSERT OR UPDATE ON artists + FOR EACH ROW + EXECUTE PROCEDURE tsvector_update_trigger('other_names_index', 'public.danbooru', 'other_names'); + + +-- +-- Name: trigger_comments_on_update; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trigger_comments_on_update + BEFORE INSERT OR UPDATE ON comments + FOR EACH ROW + EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'pg_catalog.english', 'body'); + + -- -- Name: trigger_posts_on_tag_index_update; Type: TRIGGER; Schema: public; Owner: - -- @@ -1384,6 +1809,16 @@ CREATE TRIGGER trigger_posts_on_tag_index_update EXECUTE PROCEDURE tsvector_update_trigger('tag_index', 'public.danbooru', 'tag_string', 'fav_string', 'pool_string', 'uploader_string', 'approver_string'); +-- +-- Name: trigger_wiki_pages_on_update; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trigger_wiki_pages_on_update + BEFORE INSERT OR UPDATE ON wiki_pages + FOR EACH ROW + EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'public.danbooru', 'body', 'title'); + + -- -- PostgreSQL database dump complete -- @@ -1406,4 +1841,16 @@ INSERT INTO schema_migrations (version) VALUES ('20100211181944'); INSERT INTO schema_migrations (version) VALUES ('20100211191709'); -INSERT INTO schema_migrations (version) VALUES ('20100211191716'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20100211191716'); + +INSERT INTO schema_migrations (version) VALUES ('20100213181847'); + +INSERT INTO schema_migrations (version) VALUES ('20100213183712'); + +INSERT INTO schema_migrations (version) VALUES ('20100214080549'); + +INSERT INTO schema_migrations (version) VALUES ('20100214080557'); + +INSERT INTO schema_migrations (version) VALUES ('20100214080605'); + +INSERT INTO schema_migrations (version) VALUES ('20100215182234'); \ No newline at end of file diff --git a/db/migrate/20100213181847_create_comments.rb b/db/migrate/20100213181847_create_comments.rb new file mode 100644 index 000000000..c3edea4b6 --- /dev/null +++ b/db/migrate/20100213181847_create_comments.rb @@ -0,0 +1,21 @@ +class CreateComments < ActiveRecord::Migration + def self.up + create_table :comments do |t| + t.column :post_id, :integer, :null => false + t.column :creator_id, :integer, :null => false + t.column :body, :text, :null => false + t.column :ip_addr, "inet", :null => false + t.column :body_index, "tsvector", :null => false + t.column :score, :integer, :null => false, :default => 0 + t.timestamps + end + + add_index :comments, :post_id + execute "CREATE INDEX index_comments_on_body_index ON comments USING GIN (body_index)" + execute "CREATE TRIGGER trigger_comments_on_update BEFORE INSERT OR UPDATE ON comments FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'pg_catalog.english', 'body')" + end + + def self.down + drop_table :comments + end +end diff --git a/db/migrate/20100213183712_create_comment_votes.rb b/db/migrate/20100213183712_create_comment_votes.rb new file mode 100644 index 000000000..48d8b4940 --- /dev/null +++ b/db/migrate/20100213183712_create_comment_votes.rb @@ -0,0 +1,15 @@ +class CreateCommentVotes < ActiveRecord::Migration + def self.up + create_table :comment_votes do |t| + t.column :comment_id, :integer, :null => false + t.column :user_id, :integer, :null => false + t.timestamps + end + + add_index :comment_votes, :user_id + end + + def self.down + drop_table :comment_votes + end +end diff --git a/db/migrate/20100214080549_create_artists.rb b/db/migrate/20100214080549_create_artists.rb new file mode 100644 index 000000000..39168b47f --- /dev/null +++ b/db/migrate/20100214080549_create_artists.rb @@ -0,0 +1,22 @@ +class CreateArtists < ActiveRecord::Migration + def self.up + create_table :artists do |t| + t.column :name, :string, :null => false + t.column :creator_id, :integer, :null => false + t.column :is_active, :boolean, :null => false, :default => true + t.column :other_names, :text + t.column :other_names_index, "tsvector" + t.column :group_name, :string + t.timestamps + end + + add_index :artists, :name, :unique => true + add_index :artists, :group_name + execute "CREATE INDEX index_artists_on_other_names_index ON artists USING GIN (other_names_index)" + execute "CREATE TRIGGER trigger_artists_on_update BEFORE INSERT OR UPDATE ON artists FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('other_names_index', 'public.danbooru', 'other_names')" + end + + def self.down + drop_table :artists + end +end diff --git a/db/migrate/20100214080557_create_artist_versions.rb b/db/migrate/20100214080557_create_artist_versions.rb new file mode 100644 index 000000000..958da7b53 --- /dev/null +++ b/db/migrate/20100214080557_create_artist_versions.rb @@ -0,0 +1,23 @@ +class CreateArtistVersions < ActiveRecord::Migration + def self.up + create_table :artist_versions do |t| + t.column :artist_id, :integer, :null => false + t.column :name, :string, :null => false + t.column :updater_id, :integer, :null => false + t.column :updater_ip_addr, "inet", :null => false + t.column :is_active, :boolean, :null => false, :default => true + t.column :other_names, :text + t.column :group_name, :string + t.column :url_string, :text + t.timestamps + end + + add_index :artist_versions, :artist_id + add_index :artist_versions, :name + add_index :artist_versions, :updater_id + end + + def self.down + drop_table :artist_versions + end +end diff --git a/db/migrate/20100214080605_create_artist_urls.rb b/db/migrate/20100214080605_create_artist_urls.rb new file mode 100644 index 000000000..be9ee64ed --- /dev/null +++ b/db/migrate/20100214080605_create_artist_urls.rb @@ -0,0 +1,17 @@ +class CreateArtistUrls < ActiveRecord::Migration + def self.up + create_table :artist_urls do |t| + t.column :artist_id, :integer, :null => false + t.column :url, :text, :null => false + t.column :normalized_url, :text, :null => false + t.timestamps + end + + add_index :artist_urls, :artist_id + add_index :artist_urls, :normalized_url + end + + def self.down + drop_table :artist_urls + end +end diff --git a/db/migrate/20100215182234_create_wiki_pages.rb b/db/migrate/20100215182234_create_wiki_pages.rb new file mode 100644 index 000000000..03b4e2a24 --- /dev/null +++ b/db/migrate/20100215182234_create_wiki_pages.rb @@ -0,0 +1,20 @@ +class CreateWikiPages < ActiveRecord::Migration + def self.up + create_table :wiki_pages do |t| + t.column :creator_id, :integer, :null => false + t.column :title, :string, :null => false + t.column :body, :text, :null => false + t.column :body_index, "tsvector", :null => false + t.column :is_locked, :boolean, :null => false, :default => false + t.timestamps + end + + add_index :wiki_pages, :title, :unique => true + execute "CREATE INDEX index_wiki_pages_on_body_index_index ON wiki_pages USING GIN (body_index)" + execute "CREATE TRIGGER trigger_wiki_pages_on_update BEFORE INSERT OR UPDATE ON wiki_pages FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'public.danbooru', 'body', 'title')" + end + + def self.down + drop_table :wiki_pages + end +end diff --git a/test/factories/artist.rb b/test/factories/artist.rb new file mode 100644 index 000000000..a237ad1e8 --- /dev/null +++ b/test/factories/artist.rb @@ -0,0 +1,7 @@ +Factory.define(:artist) do |f| + f.name {Faker::Name.first_name} + f.creator {|x| x.association(:user)} + f.updater_id {|x| x.creator_id} + f.updater_ip_addr "127.0.0.1" + f.is_active true +end diff --git a/test/factories/artist_url.rb b/test/factories/artist_url.rb new file mode 100644 index 000000000..2aa5d3cbc --- /dev/null +++ b/test/factories/artist_url.rb @@ -0,0 +1,3 @@ +Factory.define(:artist_url) do |f| + f.url {Faker::Internet.domain_name} +end diff --git a/test/factories/comment.rb b/test/factories/comment.rb new file mode 100644 index 000000000..6ad7962b1 --- /dev/null +++ b/test/factories/comment.rb @@ -0,0 +1,7 @@ +Factory.define(:comment) do |f| + f.creator {|x| x.association(:user)} + f.post {|x| x.association(:post)} + f.body {Faker::Lorem.sentences} + f.ip_addr "127.0.0.1" + f.score 0 +end diff --git a/test/unit/artist_test.rb b/test/unit/artist_test.rb new file mode 100644 index 000000000..21ca04bfb --- /dev/null +++ b/test/unit/artist_test.rb @@ -0,0 +1,91 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ArtistTest < ActiveSupport::TestCase + context "An artist" do + setup do + MEMCACHE.flush_all + end + + should "normalize its name" do + artist = Factory.create(:artist, :name => " AAA BBB ") + assert_equal("aaa_bbb", artist.name) + end + + should "resolve ambiguous urls" do + bobross = Factory.create(:artist, :name => "bob_ross", :url_string => "http://artists.com/bobross/image.jpg") + bob = Factory.create(:artist, :name => "bob", :url_string => "http://artists.com/bob/image.jpg") + matches = Artist.find_all_by_url("http://artists.com/bob/test.jpg") + assert_equal(1, matches.size) + assert_equal("bob", matches.first.name) + end + + should "parse urls" do + artist = Factory.create(:artist, :name => "rembrandt", :url_string => "http://rembrandt.com/test.jpg http://aaa.com") + artist.reload + assert_equal(["http://aaa.com", "http://rembrandt.com/test.jpg"], artist.artist_urls.map(&:to_s).sort) + end + + should "make sure old urls are deleted" do + artist = Factory.create(:artist, :name => "rembrandt", :url_string => "http://rembrandt.com/test.jpg") + artist.update_attributes( + :updater_id => artist.creator_id, + :updater_ip_addr => "127.0.0.1", + :url_string => "http://not.rembrandt.com/test.jpg" + ) + artist.reload + assert_equal(["http://not.rembrandt.com/test.jpg"], artist.artist_urls.map(&:to_s).sort) + end + + should "find matches by url" do + a1 = Factory.create(:artist, :name => "rembrandt", :url_string => "http://rembrandt.com/test.jpg") + a2 = Factory.create(:artist, :name => "subway", :url_string => "http://subway.com/test.jpg") + + assert_equal(["rembrandt"], Artist.find_all_by_url("http://rembrandt.com/test.jpg").map(&:name)) + assert_equal(["rembrandt"], Artist.find_all_by_url("http://rembrandt.com/another.jpg").map(&:name)) + assert_equal([], Artist.find_all_by_url("http://nonexistent.com/test.jpg").map(&:name)) + end + + should "not allow duplicates" do + Factory.create(:artist, :name => "warhol", :url_string => "http://warhol.com/a/image.jpg\nhttp://warhol.com/b/image.jpg") + assert_equal(["warhol"], Artist.find_all_by_url("http://warhol.com/test.jpg").map(&:name)) + end + + should "hide deleted artists" do + Factory.create(:artist, :name => "warhol", :url_string => "http://warhol.com/a/image.jpg", :is_active => false) + assert_equal([], Artist.find_all_by_url("http://warhol.com/a/image.jpg").map(&:name)) + end + + should "normalize its other names" do + artist = Factory.create(:artist, :name => "a1", :other_names => "aaa, bbb, ccc ddd") + assert_equal("aaa bbb ccc_ddd", artist.other_names) + end + + should "search on other names should return matches" do + artist = Factory.create(:artist, :name => "artist", :other_names => "aaa, ccc ddd") + assert_not_nil(Artist.find_by_any_name("name:artist")) + assert_nil(Artist.find_by_any_name("name:aaa")) + assert_nil(Artist.find_by_any_name("name:ccc_ddd")) + assert_nil(Artist.find_by_any_name("other:artist")) + assert_not_nil(Artist.find_by_any_name("other:aaa")) + assert_not_nil(Artist.find_by_any_name("other:ccc_ddd")) + end + + should "search on group name and return matches" do + cat_or_fish = Factory.create(:artist, :name => "cat_or_fish") + yuu = Factory.create(:artist, :name => "yuu", :group_name => "cat_or_fish") + cat_or_fish.reload + assert_equal("yuu", cat_or_fish.member_names) + assert_not_nil(Artist.find_by_any_name("group:cat_or_fish")) + end + + should "have an associated wiki" do + user = Factory.create(:user) + artist = Factory.create(:artist, :name => "max", :wiki_page_attributes => {:body => "this is max", :creator_id => user.id, :updater_id => user.id, :updater_ip_addr => "127.0.0.1"}) + assert_not_nil(artist.wiki_page) + assert_equal("this is max", artist.wiki_page.body) + + artist.update_attributes(:wiki_page_attributes => {:id => artist.wiki_page.id, :body => "this is hoge mark ii", :updater_id => user.id, :updater_ip_addr => "127.0.0.1"}) + assert_equal("this is hoge mark ii", artist.wiki_page(true).body) + end + end +end diff --git a/test/unit/artist_url_test.rb b/test/unit/artist_url_test.rb new file mode 100644 index 000000000..2ab0f91ab --- /dev/null +++ b/test/unit/artist_url_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class ArtistUrlTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/test/unit/artist_version_test.rb b/test/unit/artist_version_test.rb new file mode 100644 index 000000000..37e89e380 --- /dev/null +++ b/test/unit/artist_version_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class ArtistVersionTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb new file mode 100644 index 000000000..86380482e --- /dev/null +++ b/test/unit/comment_test.rb @@ -0,0 +1,60 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class CommentTest < ActiveSupport::TestCase + context "A comment" do + setup do + MEMCACHE.flush_all + end + + should "be created" do + comment = Factory.build(:comment) + comment.save + assert(comment.errors.empty?, comment.errors.full_messages.join(", ")) + end + + should "not bump the parent post" do + post = Factory.create(:post) + comment = Factory.create(:comment, :do_not_bump_post => "1", :post => post) + post.reload + assert_nil(post.last_commented_at) + + comment = Factory.create(:comment, :post => post) + post.reload + assert_not_nil(post.last_commented_at) + end + + should "not update the post after exceeding the threshold" do + Danbooru.config.stubs(:comment_threshold).returns(1) + p = Factory.create(:post) + c1 = Factory.create(:comment, :post => p) + sleep 1 + c2 = Factory.create(:comment, :post => p) + p.reload + assert_equal(c1.created_at.to_s, p.last_commented_at.to_s) + end + + should "not allow duplicate votes" do + user = Factory.create(:user) + post = Factory.create(:post) + c1 = Factory.create(:comment, :post => post) + assert_nothing_raised {c1.vote!(user, true)} + assert_raise(Comment::VotingError) {c1.vote!(user, true)} + assert_equal(1, CommentVote.count) + + c2 = Factory.create(:comment, :post => post) + assert_nothing_raised {c2.vote!(user, true)} + assert_equal(2, CommentVote.count) + end + + should "be searchable" do + c1 = Factory.create(:comment, :body => "aaa bbb ccc") + c2 = Factory.create(:comment, :body => "aaa ddd") + c3 = Factory.create(:comment, :body => "eee") + + matches = Comment.search_body("aaa") + assert_equal(2, matches.count) + assert_equal(c2.id, matches.all[0].id) + assert_equal(c1.id, matches.all[1].id) + end + end +end diff --git a/test/unit/comment_vote_test.rb b/test/unit/comment_vote_test.rb new file mode 100644 index 000000000..b8e167410 --- /dev/null +++ b/test/unit/comment_vote_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class CommentVoteTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/test/unit/wiki_page_test.rb b/test/unit/wiki_page_test.rb new file mode 100644 index 000000000..f97bcdc0c --- /dev/null +++ b/test/unit/wiki_page_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class WikiPageTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end