search: fix various metatag search issues.

* Support negating the child: and embedded: metatags.
* Fix approver:<any|none>, disapproved:<reason>, commentary:<type> being
  case sensitive.
* Fix child:garbage, locked:garbage, embedded:garbage returning all
  posts instead of no posts.
* Fix not being able to use source:, locked:, or -id: twice in the same
  search.
This commit is contained in:
evazion
2020-04-22 18:07:59 -05:00
parent a471cdd81e
commit d355c0e221
2 changed files with 168 additions and 82 deletions

View File

@@ -38,7 +38,9 @@ class PostQueryBuilder
-filetype filetype
-disapproved disapproved
-parent parent
-child child
-search search
-embedded embedded
md5
width
height
@@ -52,9 +54,7 @@ class PostQueryBuilder
order
limit
tagcount
child
pixiv_id pixiv
embedded
] + TagCategory.short_name_list.map {|x| "#{x}tags"} + COUNT_METATAGS + COUNT_METATAG_SYNONYMS
ORDER_METATAGS = %w[
@@ -99,9 +99,10 @@ class PostQueryBuilder
end
def user_matches(field, username)
if username == "any"
case username.downcase
when "any"
Post.where.not(field => nil)
elsif username == "none"
when "none"
Post.where(field => nil)
else
Post.where(field => User.name_matches(username))
@@ -188,13 +189,56 @@ class PostQueryBuilder
end
end
def disapproved_matches(query)
if query.downcase.in?(PostDisapproval::REASONS)
Post.where(disapprovals: PostDisapproval.where(reason: query.downcase))
elsif User.normalize_name(query) == CurrentUser.user.name
Post.where(disapprovals: PostDisapproval.where(user: CurrentUser.user))
else
Post.none
end
end
def parent_matches(parent)
if parent.downcase == "none"
case parent.downcase
when "none"
Post.where(parent: nil)
elsif parent.downcase == "any"
when "any"
Post.where.not(parent: nil)
elsif parent
when /\A\d+\z/
Post.where(id: parent).or(Post.where(parent: parent))
else
Post.none
end
end
def child_matches(child)
case child.downcase
when "none"
Post.where(has_children: false)
when "any"
Post.where(has_children: true)
else
Post.none
end
end
def source_matches(source)
case source.downcase
when "none"
Post.where_like(:source, "")
else
Post.where_ilike(:source, source + "*")
end
end
def embedded_matches(embedded)
if embedded.truthy?
Post.bit_flags_match(:has_embedded_notes, true)
elsif embedded.falsy?
Post.bit_flags_match(:has_embedded_notes, false)
else
Post.none
end
end
@@ -215,8 +259,19 @@ class PostQueryBuilder
end
end
def ordpool_matches(pool_name)
# XXX unify with Pool#posts
pool_posts = Pool.named(pool_name).joins("CROSS JOIN unnest(pools.post_ids) WITH ORDINALITY AS row(post_id, pool_index)").select(:post_id, :pool_index)
Post.joins("JOIN (#{pool_posts.to_sql}) pool_posts ON pool_posts.post_id = posts.id").order("pool_posts.pool_index ASC")
end
def favgroup_matches(query)
favgroup = FavoriteGroup.visible(CurrentUser.user).name_or_id_matches(query, CurrentUser.user)
Post.where(id: favgroup.select("unnest(post_ids)"))
end
def commentary_matches(query)
case query
case query.downcase
when "none", "false"
Post.where.not(artist_commentary: ArtistCommentary.all).or(Post.where(artist_commentary: ArtistCommentary.deleted))
when "any", "true"
@@ -230,6 +285,19 @@ class PostQueryBuilder
end
end
def locked_matches(query)
case query.downcase
when "rating"
Post.where(is_rating_locked: true)
when "note", "notes"
Post.where(is_note_locked: true)
when "status"
Post.where(is_status_locked: true)
else
Post.none
end
end
def table_for_metatag(metatag)
if metatag.in?(COUNT_METATAGS)
metatag[/(?<table>[a-z]+)_count\z/i, :table]
@@ -310,20 +378,12 @@ class PostQueryBuilder
relation = relation.merge(status_matches(query).negate)
end
if q[:source]
if q[:source] == "none"
relation = relation.where_like(:source, '')
else
relation = relation.where_ilike(:source, q[:source].downcase + "*")
end
q[:source].to_a.each do |query|
relation = relation.merge(source_matches(query))
end
if q[:source_neg]
if q[:source_neg] == "none"
relation = relation.where_not_like(:source, '')
else
relation = relation.where_not_ilike(:source, q[:source_neg].downcase + "*")
end
q[:source_neg].to_a.each do |query|
relation = relation.merge(source_matches(query).negate)
end
q[:pool_neg].to_a.each do |pool_name|
@@ -366,28 +426,12 @@ class PostQueryBuilder
relation = relation.merge(user_matches(:approver, username))
end
if q[:disapproved]
q[:disapproved].each do |disapproved|
if disapproved == CurrentUser.name
disapprovals = CurrentUser.user.post_disapprovals.select(:post_id)
else
disapprovals = PostDisapproval.where(reason: disapproved)
end
relation = relation.where("posts.id": disapprovals.select(:post_id))
end
q[:disapproved_neg].to_a.each do |query|
relation = relation.merge(disapproved_matches(query).negate)
end
if q[:disapproved_neg]
q[:disapproved_neg].each do |disapproved|
if disapproved == CurrentUser.name
disapprovals = CurrentUser.user.post_disapprovals.select(:post_id)
else
disapprovals = PostDisapproval.where(reason: disapproved)
end
relation = relation.where.not("posts.id": disapprovals.select(:post_id))
end
q[:disapproved].to_a.each do |query|
relation = relation.merge(disapproved_matches(query))
end
q[:flagger_neg].to_a.each do |username|
@@ -438,8 +482,8 @@ class PostQueryBuilder
relation = relation.merge(user_subquery_matches(ArtistCommentaryVersion.unscoped, username, field: :updater))
end
if q[:post_id_negated]
relation = relation.where("posts.id <> ?", q[:post_id_negated])
q[:id_neg].to_a.each do |id|
relation = relation.where.not(id: id)
end
q[:parent].to_a.each do |parent|
@@ -450,10 +494,12 @@ class PostQueryBuilder
relation = relation.merge(parent_matches(parent_neg).negate)
end
if q[:child] == "none"
relation = relation.where("posts.has_children = FALSE")
elsif q[:child] == "any"
relation = relation.where("posts.has_children = TRUE")
q[:child].to_a.each do |child|
relation = relation.merge(child_matches(child))
end
q[:child_neg].to_a.each do |child|
relation = relation.merge(child_matches(child).negate)
end
q[:rating].to_a.each do |rating|
@@ -464,44 +510,32 @@ class PostQueryBuilder
relation = relation.where.not(rating: rating.first.downcase)
end
if q[:locked] == "rating"
relation = relation.where("posts.is_rating_locked = TRUE")
elsif q[:locked] == "note" || q[:locked] == "notes"
relation = relation.where("posts.is_note_locked = TRUE")
elsif q[:locked] == "status"
relation = relation.where("posts.is_status_locked = TRUE")
q[:locked].to_a.each do |lock|
relation = relation.merge(locked_matches(lock))
end
if q[:locked_negated] == "rating"
relation = relation.where("posts.is_rating_locked = FALSE")
elsif q[:locked_negated] == "note" || q[:locked_negated] == "notes"
relation = relation.where("posts.is_note_locked = FALSE")
elsif q[:locked_negated] == "status"
relation = relation.where("posts.is_status_locked = FALSE")
q[:locked_neg].to_a.each do |lock|
relation = relation.merge(locked_matches(lock).negate)
end
if q[:embedded].to_s.truthy?
relation = relation.bit_flags_match(:has_embedded_notes, true)
elsif q[:embedded].to_s.falsy?
relation = relation.bit_flags_match(:has_embedded_notes, false)
q[:embedded].to_a.each do |lock|
relation = relation.merge(embedded_matches(lock))
end
if q[:ordpool].present?
pool_name = q[:ordpool]
q[:embedded_neg].to_a.each do |lock|
relation = relation.merge(embedded_matches(lock).negate)
end
# XXX unify with Pool#posts
pool_posts = Pool.named(pool_name).joins("CROSS JOIN unnest(pools.post_ids) WITH ORDINALITY AS row(post_id, pool_index)").select(:post_id, :pool_index)
relation = relation.joins("JOIN (#{pool_posts.to_sql}) pool_posts ON pool_posts.post_id = posts.id").order("pool_posts.pool_index ASC")
q[:ordpool].to_a.each do |pool_name|
relation = relation.merge(ordpool_matches(pool_name))
end
q[:favgroup_neg].to_a.each do |favgroup_name|
favgroup = FavoriteGroup.visible(CurrentUser.user).name_or_id_matches(favgroup_name, CurrentUser.user)
relation = relation.where.not(id: favgroup.select("unnest(post_ids)"))
relation = relation.merge(favgroup_matches(favgroup_name).negate)
end
q[:favgroup].to_a.each do |favgroup_name|
favgroup = FavoriteGroup.visible(CurrentUser.user).name_or_id_matches(favgroup_name, CurrentUser.user)
relation = relation.where(id: favgroup.select("unnest(post_ids)"))
relation = relation.merge(favgroup_matches(favgroup_name))
end
q[:upvoter].to_a.each do |username|
@@ -858,7 +892,8 @@ class PostQueryBuilder
q[:pool] << g2
when "ordpool"
q[:ordpool] = g2
q[:ordpool] ||= []
q[:ordpool] << g2
when "-favgroup"
q[:favgroup_neg] ||= []
@@ -909,17 +944,20 @@ class PostQueryBuilder
q[:rating] << g2
when "-locked"
q[:locked_negated] = g2.downcase
q[:locked_neg] ||= []
q[:locked_neg] << g2
when "locked"
q[:locked] = g2.downcase
q[:locked] ||= []
q[:locked] << g2
when "id"
q[:id] ||= []
q[:id] << g2
when "-id"
q[:post_id_negated] = g2.to_i
q[:id_neg] ||= []
q[:id_neg] << g2
when "width"
q[:width] ||= []
@@ -950,10 +988,12 @@ class PostQueryBuilder
q[:file_size] << g2
when "source"
q[:source] = g2
q[:source] ||= []
q[:source] << g2
when "-source"
q[:source_neg] = g2
q[:source_neg] ||= []
q[:source_neg] << g2
when "date"
q[:date] ||= []
@@ -980,7 +1020,12 @@ class PostQueryBuilder
q[:parent_neg] << g2
when "child"
q[:child] = g2.downcase
q[:child] ||= []
q[:child] << g2
when "-child"
q[:child_neg] ||= []
q[:child_neg] << g2
when "order"
g2 = g2.downcase
@@ -1004,7 +1049,12 @@ class PostQueryBuilder
q[:status] << g2
when "embedded"
q[:embedded] = g2.downcase
q[:embedded] ||= []
q[:embedded] << g2
when "-embedded"
q[:embedded_neg] ||= []
q[:embedded_neg] << g2
when "filetype"
q[:filetype] ||= []

View File

@@ -159,6 +159,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([posts[1], posts[0]], "id:#{posts[0].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}")
end
@@ -259,6 +260,11 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([child], "child:none")
assert_tag_match([parent], "child:any")
assert_tag_match([], "child:garbage")
assert_tag_match([parent], "-child:none")
assert_tag_match([child], "-child:any")
assert_tag_match([child, parent], "-child:garbage")
end
should "return posts for the favgroup:<name> metatag" do
@@ -311,6 +317,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([posts[1]], "-approver:#{users[0].name}")
assert_tag_match([posts[1], posts[0]], "approver:any")
assert_tag_match([posts[2]], "approver:none")
assert_tag_match([posts[2]], "approver:NONE")
assert_tag_match([], "approver:does_not_exist")
end
should "return posts for the commenter:<name> metatag" do
@@ -400,6 +408,9 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([post2, post1], "commentary:true")
assert_tag_match([post4, post3], "commentary:false")
assert_tag_match([post2, post1], "commentary:TRUE")
assert_tag_match([post4, post3], "commentary:FALSE")
assert_tag_match([post4, post3], "-commentary:true")
assert_tag_match([post2, post1], "-commentary:false")
@@ -512,6 +523,21 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([], "filetype:garbage")
end
should "return posts for the embedded:<true|false> metatag" do
p1 = create(:post, has_embedded_notes: true)
p2 = create(:post, has_embedded_notes: false)
assert_tag_match([p1], "embedded:true")
assert_tag_match([p2], "embedded:false")
assert_tag_match([p2], "-embedded:true")
assert_tag_match([p1], "-embedded:false")
assert_tag_match([], "embedded:false embedded:true")
assert_tag_match([], "embedded:garbage")
assert_tag_match([p2, p1], "-embedded:garbage")
end
should "return posts for the tagcount:<n> metatags" do
post = create(:post, tag_string: "artist:wokada copyright:vocaloid char:hatsune_miku twintails")
@@ -542,6 +568,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([post3, post1], "-source:abcde")
assert_tag_match([post3], "source:none")
assert_tag_match([post3], "source:NONE")
assert_tag_match([post2, post1], "-source:none")
end
@@ -649,6 +676,13 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match(all - [rating_locked], "-locked:rating")
assert_tag_match(all - [note_locked], "-locked:note")
assert_tag_match(all - [status_locked], "-locked:status")
assert_tag_match([rating_locked], "locked:RATING")
assert_tag_match([status_locked], "-locked:rating -locked:note")
assert_tag_match([], "locked:rating locked:note")
assert_tag_match([], "locked:garbage")
assert_tag_match(all, "-locked:garbage")
end
should "return posts for a upvote:<user>, downvote:<user> metatag" do
@@ -668,11 +702,13 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
disapproval = create(:post_disapproval, user: CurrentUser.user, post: disapproved, reason: "disinterest")
assert_tag_match([disapproved], "disapproved:#{CurrentUser.name}")
assert_tag_match([disapproved], "disapproved:#{CurrentUser.name.upcase}")
assert_tag_match([disapproved], "disapproved:disinterest")
assert_tag_match([], "disapproved:breaks_rules")
assert_tag_match([disapproved], "disapproved:DISINTEREST")
assert_tag_match([], "disapproved:breaks_rules")
assert_tag_match([pending], "-disapproved:#{CurrentUser.name}")
assert_tag_match([pending], "-disapproved:disinterest")
assert_tag_match([pending], "-disapproved:#{CurrentUser.name}")
assert_tag_match([pending], "-disapproved:disinterest")
assert_tag_match([disapproved, pending], "-disapproved:breaks_rules")
end
end