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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:<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[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:<any|none> 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:<name> 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:<name> 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:<n> 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:<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.0")
|
||||
assert_tag_match([], "-ratio:2.0")
|
||||
end
|
||||
|
||||
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")
|
||||
|
||||
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:<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:<text> 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user