search: allow all metatags to be negated.

Fix not being able to negate the following metatags:

* id (didn't support ranges)
* md5
* width
* height
* mpixels
* ratio
* score
* favcount
* filesize
* date
* age
* tagcount
* pixiv
This commit is contained in:
evazion
2020-04-29 02:31:15 -05:00
parent dc144f7d7d
commit e978f07068
4 changed files with 44 additions and 121 deletions

View File

@@ -19,49 +19,11 @@ class PostQueryBuilder
CATEGORY_COUNT_METATAGS = TagCategory.short_name_list.map { |category| "#{category}tags" } CATEGORY_COUNT_METATAGS = TagCategory.short_name_list.map { |category| "#{category}tags" }
METATAGS = %w[ METATAGS = %w[
-user user user approver commenter comm noter noteupdater artcomm commentaryupdater
-approver approver flagger appealer upvote downvote fav ordfav favgroup ordfavgroup pool
-commenter commenter comm ordpool note comment commentary id rating locked source status filetype
-noter noter disapproved parent child search embedded md5 width height mpixels ratio
-noteupdater noteupdater score favcount filesize date age order limit tagcount pixiv_id pixiv
-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
] + COUNT_METATAGS + COUNT_METATAG_SYNONYMS + CATEGORY_COUNT_METATAGS ] + COUNT_METATAGS + COUNT_METATAG_SYNONYMS + CATEGORY_COUNT_METATAGS
ORDER_METATAGS = %w[ ORDER_METATAGS = %w[
@@ -121,7 +83,9 @@ class PostQueryBuilder
def metatags_match(metatags, relation) def metatags_match(metatags, relation)
metatags.each do |metatag| 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 end
relation relation
@@ -131,8 +95,6 @@ class PostQueryBuilder
case name case name
when "id" when "id"
attribute_matches(value, :id) attribute_matches(value, :id)
when "-id"
Post.where.not(id: value.to_i)
when "md5" when "md5"
attribute_matches(value, :md5, :md5) attribute_matches(value, :md5, :md5)
when "width" when "width"
@@ -151,8 +113,6 @@ class PostQueryBuilder
attribute_matches(value, :file_size, :filesize) attribute_matches(value, :file_size, :filesize)
when "filetype" when "filetype"
attribute_matches(value, :file_ext, :enum) attribute_matches(value, :file_ext, :enum)
when "-filetype"
attribute_matches(value, :file_ext, :enum).negate(:nor)
when "date" when "date"
attribute_matches(value, :created_at, :date) attribute_matches(value, :created_at, :date)
when "age" when "age"
@@ -163,110 +123,60 @@ class PostQueryBuilder
attribute_matches(value, :tag_count) attribute_matches(value, :tag_count)
when "status" when "status"
status_matches(value) status_matches(value)
when "-status"
status_matches(value).negate
when "parent" when "parent"
parent_matches(value) parent_matches(value)
when "-parent"
parent_matches(value).negate
when "child" when "child"
child_matches(value) child_matches(value)
when "-child"
child_matches(value).negate
when "rating" when "rating"
Post.where(rating: value.first.downcase) Post.where(rating: value.first.downcase)
when "-rating"
Post.where(rating: value.first.downcase).negate
when "locked" when "locked"
locked_matches(value) locked_matches(value)
when "-locked"
locked_matches(value).negate
when "embedded" when "embedded"
embedded_matches(value) embedded_matches(value)
when "-embedded"
embedded_matches(value).negate
when "source" when "source"
source_matches(value, quoted) source_matches(value, quoted)
when "-source"
source_matches(value, quoted).negate
when "disapproved" when "disapproved"
disapproved_matches(value) disapproved_matches(value)
when "-disapproved"
disapproved_matches(value).negate
when "commentary" when "commentary"
commentary_matches(value, quoted) commentary_matches(value, quoted)
when "-commentary"
commentary_matches(value, quoted).negate
when "note" when "note"
note_matches(value) note_matches(value)
when "-note"
note_matches(value).negate
when "comment" when "comment"
comment_matches(value) comment_matches(value)
when "-comment"
comment_matches(value).negate
when "search" when "search"
saved_search_matches(value) saved_search_matches(value)
when "-search"
saved_search_matches(value).negate
when "pool" when "pool"
pool_matches(value) pool_matches(value)
when "-pool"
pool_matches(value).negate
when "ordpool" when "ordpool"
ordpool_matches(value) ordpool_matches(value)
when "favgroup" when "favgroup"
favgroup_matches(value) favgroup_matches(value)
when "-favgroup"
favgroup_matches(value).negate
when "ordfavgroup" when "ordfavgroup"
ordfavgroup_matches(value) ordfavgroup_matches(value)
when "fav" when "fav"
favorites_include(value) favorites_include(value)
when "-fav"
favorites_exclude(value)
when "ordfav" when "ordfav"
ordfav_matches(value) ordfav_matches(value)
when "user" when "user"
user_matches(:uploader, value) user_matches(:uploader, value)
when "-user"
user_matches(:uploader, value).negate
when "approver" when "approver"
user_matches(:approver, value) user_matches(:approver, value)
when "-approver"
user_matches(:approver, value).negate
when "flagger" when "flagger"
flagger_matches(value) flagger_matches(value)
when "-flagger"
flagger_matches(value).negate
when "appealer" when "appealer"
user_subquery_matches(PostAppeal.unscoped, value) user_subquery_matches(PostAppeal.unscoped, value)
when "-appealer"
user_subquery_matches(PostAppeal.unscoped, value).negate
when "commenter", "comm" when "commenter", "comm"
user_subquery_matches(Comment.unscoped, value) user_subquery_matches(Comment.unscoped, value)
when "-commenter"
user_subquery_matches(Comment.unscoped, value).negate
when "commentaryupdater", "artcomm" when "commentaryupdater", "artcomm"
user_subquery_matches(ArtistCommentaryVersion.unscoped, value, field: :updater) user_subquery_matches(ArtistCommentaryVersion.unscoped, value, field: :updater)
when "-commentaryupdater", "-artcomm"
user_subquery_matches(ArtistCommentaryVersion.unscoped, value, field: :updater).negate
when "noter" when "noter"
user_subquery_matches(NoteVersion.unscoped.where(version: 1), value, field: :updater) 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" when "noteupdater"
user_subquery_matches(NoteVersion.unscoped, value, field: :updater) user_subquery_matches(NoteVersion.unscoped, value, field: :updater)
when "-noteupdater"
user_subquery_matches(NoteVersion.unscoped, value, field: :updater).negate
when "upvoter", "upvote" when "upvoter", "upvote"
user_subquery_matches(PostVote.positive.visible(CurrentUser.user), value, field: :user) 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" when "downvoter", "downvote"
user_subquery_matches(PostVote.negative.visible(CurrentUser.user), value, field: :user) 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 when *CATEGORY_COUNT_METATAGS
short_category = name.delete_suffix("tags") short_category = name.delete_suffix("tags")
category = TagCategory.short_name_mapping[short_category] category = TagCategory.short_name_mapping[short_category]
@@ -288,11 +198,6 @@ class PostQueryBuilder
Post.where("posts.tag_index @@ to_tsquery('danbooru', E?)", query) Post.where("posts.tag_index @@ to_tsquery('danbooru', E?)", query)
end 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) def attribute_matches(value, field, type = :integer)
operator, *args = parse_metatag_value(value, type) operator, *args = parse_metatag_value(value, type)
Post.where_operator(field, operator, *args) Post.where_operator(field, operator, *args)
@@ -465,16 +370,6 @@ class PostQueryBuilder
end end
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) def ordfav_matches(username)
user = User.find_by_name(username) user = User.find_by_name(username)
favorites_include(username).joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC") 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? def hide_deleted_posts?
return false if CurrentUser.admin_mode? 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? return CurrentUser.user.hide_deleted_posts?
end end
@@ -726,8 +620,9 @@ class PostQueryBuilder
until scanner.eos? until scanner.eos?
scanner.skip(/ +/) scanner.skip(/ +/)
if scanner.scan(/(#{METATAGS.join("|")}):/io) if scanner.scan(/(-)?(#{METATAGS.join("|")}):/io)
metatag = scanner.captures.first.downcase operator = scanner.captures.first
metatag = scanner.captures.second.downcase
if scanner.scan(/"(.+)"/) || scanner.scan(/'(.+)'/) if scanner.scan(/"(.+)"/) || scanner.scan(/'(.+)'/)
value = scanner.captures.first value = scanner.captures.first
@@ -746,7 +641,7 @@ class PostQueryBuilder
end end
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(/([-~])?([^ ]+)/) elsif scanner.scan(/([-~])?([^ ]+)/)
operator = scanner.captures.first operator = scanner.captures.first
tag = scanner.captures.second tag = scanner.captures.second

View File

@@ -1069,7 +1069,7 @@ class Post < ApplicationRecord
def fast_count(tags = "", timeout: 1_000, raise_on_timeout: false, skip_cache: false) def fast_count(tags = "", timeout: 1_000, raise_on_timeout: false, skip_cache: false)
tags = tags.to_s tags = tags.to_s
tags += " rating:s" if CurrentUser.safe_mode? 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 tags = PostQueryBuilder.new(tags).normalize_query
# Optimize some cases. these are just estimates but at these # Optimize some cases. these are just estimates but at these

View File

@@ -109,7 +109,7 @@ module Danbooru
# Return true if the given tag shouldn't count against the user's tag search limit. # Return true if the given tag shouldn't count against the user's tag search limit.
def is_unlimited_tag?(term) 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 end
# After this many pages, the paginator will switch to sequential mode. # After this many pages, the paginator will switch to sequential mode.

View File

@@ -147,7 +147,6 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([posts[2]], "id:>#{posts[1].id}") assert_tag_match([posts[2]], "id:>#{posts[1].id}")
assert_tag_match([posts[0]], "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[2], posts[1]], "id:>=#{posts[1].id}")
assert_tag_match([posts[1], posts[0]], "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}") 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[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[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([], "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[0]], "-id:#{posts[1].id} -id:#{posts[2].id}")
assert_tag_match([posts[1]], "id:>#{posts[0].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[0]], "commenter:#{users[0].name}")
assert_tag_match([posts[1]], "commenter:#{users[1].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 end
should "return posts for the commenter:<any|none> metatag" do should "return posts for the commenter:<any|none> metatag" do
@@ -369,6 +377,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([posts[0]], "noter:#{users[0].name}") 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[1].name}")
assert_tag_match([posts[1]], "-noter:#{users[0].name}")
assert_tag_match([posts[0]], "-noter:#{users[1].name}")
end end
should "return posts for the noter:<any|none> metatag" do should "return posts for the noter:<any|none> metatag" do
@@ -377,7 +387,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
create(:note, post: posts[1], is_active: false) create(:note, post: posts[1], is_active: false)
assert_tag_match(posts.reverse, "noter:any") assert_tag_match(posts.reverse, "noter:any")
assert_tag_match(posts.reverse, "-noter:none")
assert_tag_match([], "noter:none") assert_tag_match([], "noter:none")
assert_tag_match([], "-noter:any")
end end
should "return posts for the noteupdater:<name> metatag" do should "return posts for the noteupdater:<name> metatag" do
@@ -404,6 +416,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([posts[1], posts[0]], "notes:1") assert_tag_match([posts[1], posts[0]], "notes:1")
assert_tag_match([posts[0]], "active_notes:1") assert_tag_match([posts[0]], "active_notes:1")
assert_tag_match([posts[1]], "deleted_notes:1") assert_tag_match([posts[1]], "deleted_notes:1")
assert_tag_match([posts[2]], "-note_count:1")
end end
should "return posts for the commentaryupdater:<name> metatag" do should "return posts for the commentaryupdater:<name> metatag" do
@@ -510,6 +524,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
post = create(:post, created_at: Time.parse("2017-01-01 12:00")) post = create(:post, created_at: Time.parse("2017-01-01 12:00"))
assert_tag_match([post], "date:2017-01-01") assert_tag_match([post], "date:2017-01-01")
assert_tag_match([], "-date:2017-01-01")
end end
should "return posts for the age:<n> metatag" do should "return posts for the age:<n> metatag" do
@@ -534,6 +549,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([], "age:>=1y") assert_tag_match([], "age:>=1y")
assert_tag_match([], "age:1y..2y") assert_tag_match([], "age:1y..2y")
assert_tag_match([], "age:>1y age:<1y") assert_tag_match([], "age:>1y age:<1y")
assert_tag_match([post], "-age:>1y")
assert_tag_match([], "-age:<1y")
end end
should "return posts for the ratio:<x:y> metatag" do should "return posts for the ratio:<x:y> metatag" do
@@ -541,6 +559,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([post], "ratio:2:1") assert_tag_match([post], "ratio:2:1")
assert_tag_match([post], "ratio:2.0") assert_tag_match([post], "ratio:2.0")
assert_tag_match([], "-ratio:2.0")
end end
should "return posts for the status:<type> metatag" do should "return posts for the status:<type> metatag" do
@@ -580,6 +599,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
create(:post_disapproval, user: CurrentUser.user, post: disapproved, reason: "disinterest") create(:post_disapproval, user: CurrentUser.user, post: disapproved, reason: "disinterest")
assert_tag_match([pending, flagged], "status:unmoderated") assert_tag_match([pending, flagged], "status:unmoderated")
assert_tag_match([disapproved], "-status:unmoderated")
end end
should "respect the 'Deleted post filter' option when using the status: metatag" do 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], "copytags:1")
assert_tag_match([post], "chartags:1") assert_tag_match([post], "chartags:1")
assert_tag_match([post], "gentags:1") assert_tag_match([post], "gentags:1")
assert_tag_match([], "-gentags:1")
assert_tag_match([], "-tagcount:4")
end end
should "return posts for the md5:<md5> metatag" do should "return posts for the md5:<md5> metatag" do
@@ -652,6 +675,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([post1], "md5:ABCD") assert_tag_match([post1], "md5:ABCD")
assert_tag_match([post1], "md5:123,abcd") assert_tag_match([post1], "md5:123,abcd")
assert_tag_match([], "md5:abcd md5:xyz") assert_tag_match([], "md5:abcd md5:xyz")
assert_tag_match([post2], "-md5:abcd")
end end
should "return posts for a source:<text> search" do should "return posts for a source:<text> search" do
@@ -790,6 +815,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([downvoted], "downvote:#{CurrentUser.name}") assert_tag_match([downvoted], "downvote:#{CurrentUser.name}")
assert_tag_match([], "upvote:nobody upvote:#{CurrentUser.name}") assert_tag_match([], "upvote:nobody upvote:#{CurrentUser.name}")
assert_tag_match([], "downvote:nobody downvote:#{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
end end