diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index 456e6d5b4..429ba59b0 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -19,49 +19,11 @@ class PostQueryBuilder CATEGORY_COUNT_METATAGS = TagCategory.short_name_list.map { |category| "#{category}tags" } METATAGS = %w[ - -user user - -approver approver - -commenter commenter comm - -noter noter - -noteupdater noteupdater - -artcomm artcomm - -commentaryupdater commentaryupdater - -flagger flagger - -appealer appealer - -upvote upvote - -downvote downvote - -fav fav - -ordfav ordfav - -favgroup favgroup ordfavgroup - -pool pool ordpool - -note note - -comment comment - -commentary commentary - -id id - -rating rating - -locked locked - -source source - -status status - -filetype filetype - -disapproved disapproved - -parent parent - -child child - -search search - -embedded embedded - md5 - width - height - mpixels - ratio - score - favcount - filesize - date - age - order - limit - tagcount - pixiv_id pixiv + user approver commenter comm noter noteupdater artcomm commentaryupdater + flagger appealer upvote downvote fav ordfav favgroup ordfavgroup pool + ordpool note comment commentary id rating locked source status filetype + disapproved parent child search embedded md5 width height mpixels ratio + score favcount filesize date age order limit tagcount pixiv_id pixiv ] + COUNT_METATAGS + COUNT_METATAG_SYNONYMS + CATEGORY_COUNT_METATAGS ORDER_METATAGS = %w[ @@ -121,7 +83,9 @@ class PostQueryBuilder def metatags_match(metatags, relation) metatags.each do |metatag| - relation = relation.and(metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted)) + clause = metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted) + clause = clause.negate if metatag.negated + relation = relation.and(clause) end relation @@ -131,8 +95,6 @@ class PostQueryBuilder case name when "id" attribute_matches(value, :id) - when "-id" - Post.where.not(id: value.to_i) when "md5" attribute_matches(value, :md5, :md5) when "width" @@ -151,8 +113,6 @@ class PostQueryBuilder attribute_matches(value, :file_size, :filesize) when "filetype" attribute_matches(value, :file_ext, :enum) - when "-filetype" - attribute_matches(value, :file_ext, :enum).negate(:nor) when "date" attribute_matches(value, :created_at, :date) when "age" @@ -163,110 +123,60 @@ class PostQueryBuilder attribute_matches(value, :tag_count) when "status" status_matches(value) - when "-status" - status_matches(value).negate when "parent" parent_matches(value) - when "-parent" - parent_matches(value).negate when "child" child_matches(value) - when "-child" - child_matches(value).negate when "rating" Post.where(rating: value.first.downcase) - when "-rating" - Post.where(rating: value.first.downcase).negate when "locked" locked_matches(value) - when "-locked" - locked_matches(value).negate when "embedded" embedded_matches(value) - when "-embedded" - embedded_matches(value).negate when "source" source_matches(value, quoted) - when "-source" - source_matches(value, quoted).negate when "disapproved" disapproved_matches(value) - when "-disapproved" - disapproved_matches(value).negate when "commentary" commentary_matches(value, quoted) - when "-commentary" - commentary_matches(value, quoted).negate when "note" note_matches(value) - when "-note" - note_matches(value).negate when "comment" comment_matches(value) - when "-comment" - comment_matches(value).negate when "search" saved_search_matches(value) - when "-search" - saved_search_matches(value).negate when "pool" pool_matches(value) - when "-pool" - pool_matches(value).negate when "ordpool" ordpool_matches(value) when "favgroup" favgroup_matches(value) - when "-favgroup" - favgroup_matches(value).negate when "ordfavgroup" ordfavgroup_matches(value) when "fav" favorites_include(value) - when "-fav" - favorites_exclude(value) when "ordfav" ordfav_matches(value) when "user" user_matches(:uploader, value) - when "-user" - user_matches(:uploader, value).negate when "approver" user_matches(:approver, value) - when "-approver" - user_matches(:approver, value).negate when "flagger" flagger_matches(value) - when "-flagger" - flagger_matches(value).negate when "appealer" user_subquery_matches(PostAppeal.unscoped, value) - when "-appealer" - user_subquery_matches(PostAppeal.unscoped, value).negate when "commenter", "comm" user_subquery_matches(Comment.unscoped, value) - when "-commenter" - user_subquery_matches(Comment.unscoped, value).negate when "commentaryupdater", "artcomm" user_subquery_matches(ArtistCommentaryVersion.unscoped, value, field: :updater) - when "-commentaryupdater", "-artcomm" - user_subquery_matches(ArtistCommentaryVersion.unscoped, value, field: :updater).negate when "noter" user_subquery_matches(NoteVersion.unscoped.where(version: 1), value, field: :updater) - when "-noter" - user_subquery_matches(NoteVersion.unscoped.where(version: 1), value, field: :updater).negate when "noteupdater" user_subquery_matches(NoteVersion.unscoped, value, field: :updater) - when "-noteupdater" - user_subquery_matches(NoteVersion.unscoped, value, field: :updater).negate when "upvoter", "upvote" user_subquery_matches(PostVote.positive.visible(CurrentUser.user), value, field: :user) - when "-upvoter", "-upvote" - user_subquery_matches(PostVote.positive.visible(CurrentUser.user), value, field: :user).negate when "downvoter", "downvote" user_subquery_matches(PostVote.negative.visible(CurrentUser.user), value, field: :user) - when "-downvoter", "-downvote" - user_subquery_matches(PostVote.negative.visible(CurrentUser.user), value, field: :user).negate when *CATEGORY_COUNT_METATAGS short_category = name.delete_suffix("tags") category = TagCategory.short_name_mapping[short_category] @@ -288,11 +198,6 @@ class PostQueryBuilder Post.where("posts.tag_index @@ to_tsquery('danbooru', E?)", query) end - def tags_exclude(*tags) - query = tags.map(&:to_escaped_for_tsquery).join(" | ") - Post.where("posts.tag_index @@ to_tsquery('danbooru', E?)", "!(#{query})") - end - def attribute_matches(value, field, type = :integer) operator, *args = parse_metatag_value(value, type) Post.where_operator(field, operator, *args) @@ -465,16 +370,6 @@ class PostQueryBuilder end end - def favorites_exclude(username) - favuser = User.find_by_name(username) - - if favuser.present? && Pundit.policy!([CurrentUser.user, nil], favuser).can_see_favorites? - tags_exclude("fav:#{favuser.id}") - else - Post.all - end - end - def ordfav_matches(username) user = User.find_by_name(username) favorites_include(username).joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC") @@ -540,8 +435,7 @@ class PostQueryBuilder def hide_deleted_posts? return false if CurrentUser.admin_mode? - return false if find_metatag("status").to_s.downcase.in?(%w[deleted active any all]) - return false if find_metatag("-status").to_s.downcase.in?(%w[deleted active any all]) + return false if find_metatag(:status).to_s.downcase.in?(%w[deleted active any all]) return CurrentUser.user.hide_deleted_posts? end @@ -726,8 +620,9 @@ class PostQueryBuilder until scanner.eos? scanner.skip(/ +/) - if scanner.scan(/(#{METATAGS.join("|")}):/io) - metatag = scanner.captures.first.downcase + if scanner.scan(/(-)?(#{METATAGS.join("|")}):/io) + operator = scanner.captures.first + metatag = scanner.captures.second.downcase if scanner.scan(/"(.+)"/) || scanner.scan(/'(.+)'/) value = scanner.captures.first @@ -746,7 +641,7 @@ class PostQueryBuilder end end - terms << OpenStruct.new({ type: :metatag, name: metatag, value: value, quoted: quoted }) + terms << OpenStruct.new(type: :metatag, name: metatag, value: value, negated: (operator == "-"), quoted: quoted) elsif scanner.scan(/([-~])?([^ ]+)/) operator = scanner.captures.first tag = scanner.captures.second diff --git a/app/models/post.rb b/app/models/post.rb index b40e89ada..8c3a6f125 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1069,7 +1069,7 @@ class Post < ApplicationRecord def fast_count(tags = "", timeout: 1_000, raise_on_timeout: false, skip_cache: false) tags = tags.to_s tags += " rating:s" if CurrentUser.safe_mode? - tags += " -status:deleted" if CurrentUser.hide_deleted_posts? && !PostQueryBuilder.new(tags).has_metatag?("status", "-status") + tags += " -status:deleted" if CurrentUser.hide_deleted_posts? && !PostQueryBuilder.new(tags).has_metatag?("status") tags = PostQueryBuilder.new(tags).normalize_query # Optimize some cases. these are just estimates but at these diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index 2dab0f379..0762e1edc 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -109,7 +109,7 @@ module Danbooru # Return true if the given tag shouldn't count against the user's tag search limit. def is_unlimited_tag?(term) - term.type == :metatag && term.name.in?(%w[-status status rating limit]) + term.type == :metatag && term.name.in?(%w[status rating limit]) end # After this many pages, the paginator will switch to sequential mode. diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index 226cedd71..2e346494f 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -147,7 +147,6 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[2]], "id:>#{posts[1].id}") assert_tag_match([posts[0]], "id:<#{posts[1].id}") - assert_tag_match([posts[2], posts[0]], "-id:#{posts[1].id}") assert_tag_match([posts[2], posts[1]], "id:>=#{posts[1].id}") assert_tag_match([posts[1], posts[0]], "id:<=#{posts[1].id}") assert_tag_match([posts[2], posts[0]], "id:#{posts[0].id},#{posts[2].id}") @@ -158,6 +157,13 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[1], posts[0]], "id:#{posts[1].id}..#{posts[0].id}") assert_tag_match([posts[1], posts[0]], "id:#{posts[0].id}...#{posts[2].id}") + assert_tag_match([posts[1], posts[0]], "-id:>#{posts[1].id}") + assert_tag_match([posts[2], posts[1]], "-id:<#{posts[1].id}") + assert_tag_match([posts[0]], "-id:>=#{posts[1].id}") + assert_tag_match([posts[2]], "-id:<=#{posts[1].id}") + assert_tag_match([posts[0]], "-id:#{posts[1].id}..#{posts[2].id}") + assert_tag_match([posts[0]], "-id:#{posts[1].id},#{posts[2].id}") + assert_tag_match([], "id:#{posts[0].id} id:#{posts[2].id}") assert_tag_match([posts[0]], "-id:#{posts[1].id} -id:#{posts[2].id}") assert_tag_match([posts[1]], "id:>#{posts[0].id} id:<#{posts[2].id}") @@ -349,6 +355,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[0]], "commenter:#{users[0].name}") assert_tag_match([posts[1]], "commenter:#{users[1].name}") + assert_tag_match([posts[1]], "-commenter:#{users[0].name}") + assert_tag_match([posts[0]], "-commenter:#{users[1].name}") end should "return posts for the commenter: metatag" do @@ -369,6 +377,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[0]], "noter:#{users[0].name}") assert_tag_match([posts[1]], "noter:#{users[1].name}") + assert_tag_match([posts[1]], "-noter:#{users[0].name}") + assert_tag_match([posts[0]], "-noter:#{users[1].name}") end should "return posts for the noter: metatag" do @@ -377,7 +387,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase create(:note, post: posts[1], is_active: false) assert_tag_match(posts.reverse, "noter:any") + assert_tag_match(posts.reverse, "-noter:none") assert_tag_match([], "noter:none") + assert_tag_match([], "-noter:any") end should "return posts for the noteupdater: metatag" do @@ -404,6 +416,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([posts[1], posts[0]], "notes:1") assert_tag_match([posts[0]], "active_notes:1") assert_tag_match([posts[1]], "deleted_notes:1") + + assert_tag_match([posts[2]], "-note_count:1") end should "return posts for the commentaryupdater: metatag" do @@ -510,6 +524,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase post = create(:post, created_at: Time.parse("2017-01-01 12:00")) assert_tag_match([post], "date:2017-01-01") + assert_tag_match([], "-date:2017-01-01") end should "return posts for the age: metatag" do @@ -534,6 +549,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([], "age:>=1y") assert_tag_match([], "age:1y..2y") assert_tag_match([], "age:>1y age:<1y") + + assert_tag_match([post], "-age:>1y") + assert_tag_match([], "-age:<1y") end should "return posts for the ratio: metatag" do @@ -541,6 +559,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([post], "ratio:2:1") assert_tag_match([post], "ratio:2.0") + assert_tag_match([], "-ratio:2.0") end should "return posts for the status: metatag" do @@ -580,6 +599,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase create(:post_disapproval, user: CurrentUser.user, post: disapproved, reason: "disinterest") assert_tag_match([pending, flagged], "status:unmoderated") + assert_tag_match([disapproved], "-status:unmoderated") end should "respect the 'Deleted post filter' option when using the status: metatag" do @@ -642,6 +662,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([post], "copytags:1") assert_tag_match([post], "chartags:1") assert_tag_match([post], "gentags:1") + + assert_tag_match([], "-gentags:1") + assert_tag_match([], "-tagcount:4") end should "return posts for the md5: metatag" do @@ -652,6 +675,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([post1], "md5:ABCD") assert_tag_match([post1], "md5:123,abcd") assert_tag_match([], "md5:abcd md5:xyz") + + assert_tag_match([post2], "-md5:abcd") end should "return posts for a source: search" do @@ -790,6 +815,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([downvoted], "downvote:#{CurrentUser.name}") assert_tag_match([], "upvote:nobody upvote:#{CurrentUser.name}") assert_tag_match([], "downvote:nobody downvote:#{CurrentUser.name}") + + assert_tag_match([downvoted], "-upvote:#{CurrentUser.name}") + assert_tag_match([upvoted], "-downvote:#{CurrentUser.name}") end end