posts: add is: and has: metatags.

Add the following metatags:

* is:parent
* is:child
* is:safe
* is:questionable
* is:explicit
* is:sfw (same as -rating:q,e)
* is:nsfw (same as rating:q,e)
* is:active
* is:deleted
* is:pending
* is:flagged
* is:appealed
* is:banned
* is:modqueue
* is:unmoderated
* is:jpg
* is:png
* is:gif
* is:mp4
* is:webm
* is:swf
* is:zip
* has:parent
* has:children
* has:source
* has:appeals
* has:flags
* has:replacements
* has:comments
* has:commentary
* has:notes
* has:pools

All of these searches were already possible with other metatags, but these might be more convenient.
This commit is contained in:
evazion
2022-05-18 11:44:35 -05:00
parent 141044d352
commit 181639368c
5 changed files with 150 additions and 3 deletions

View File

@@ -13,12 +13,14 @@ class AutocompleteService
POST_STATUSES = %w[active deleted pending flagged appealed banned modqueue unmoderated]
STATIC_METATAGS = {
is: %w[parent child sfw nsfw] + POST_STATUSES + MediaAsset::FILE_TYPES + Post::RATINGS.values.map(&:downcase),
has: %w[parent children source appeals flags replacements comments commentary notes pools],
status: %w[any] + POST_STATUSES,
child: %w[any none] + POST_STATUSES,
parent: %w[any none] + POST_STATUSES,
rating: Post::RATINGS.values.map(&:downcase),
embedded: %w[true false],
filetype: %w[jpg png gif swf zip webm mp4],
filetype: MediaAsset::FILE_TYPES,
commentary: %w[true false translated untranslated],
disapproved: PostDisapproval::REASONS,
order: PostQueryBuilder::ORDER_METATAGS

View File

@@ -38,7 +38,7 @@ class PostQueryBuilder
ordpool note comment commentary id rating source status filetype
disapproved parent child search embedded md5 width height mpixels ratio
score upvotes downvotes favcount filesize date age order limit tagcount pixiv_id pixiv
unaliased exif duration random
unaliased exif duration random is has
] + COUNT_METATAGS + COUNT_METATAG_SYNONYMS + CATEGORY_COUNT_METATAGS
ORDER_METATAGS = %w[
@@ -121,6 +121,10 @@ class PostQueryBuilder
relation.attribute_matches(value, :tag_count)
when "duration"
relation.attribute_matches(value, "media_assets.duration", :float).joins(:media_asset)
when "is"
relation.is_matches(value, current_user)
when "has"
relation.has_matches(value)
when "status"
relation.status_matches(value, current_user)
when "parent"

View File

@@ -3,6 +3,7 @@
class MediaAsset < ApplicationRecord
class Error < StandardError; end
FILE_TYPES = %w[jpg png gif mp4 webm swf zip]
FILE_KEY_LENGTH = 9
VARIANTS = %i[preview 180x180 360x360 720x720 sample original]
MAX_VIDEO_DURATION = Danbooru.config.max_video_duration.to_i
@@ -41,7 +42,7 @@ class MediaAsset < ApplicationRecord
}
validates :md5, uniqueness: { conditions: -> { where(status: [:processing, :active]) } }
validates :file_ext, inclusion: { in: %w[jpg png gif mp4 webm swf zip], message: "File is not an image or video" }
validates :file_ext, inclusion: { in: FILE_TYPES, message: "File is not an image or video" }
validates :file_size, numericality: { less_than_or_equal_to: Danbooru.config.max_file_size, message: ->(asset, _) { "too large (size: #{asset.file_size.to_formatted_s(:human_size)}; max size: #{Danbooru.config.max_file_size.to_formatted_s(:human_size)})" } }
validates :file_key, length: { is: FILE_KEY_LENGTH }, uniqueness: true, if: :file_key_changed?
validates :duration, numericality: { less_than_or_equal_to: MAX_VIDEO_DURATION, message: "must be less than #{MAX_VIDEO_DURATION} seconds", allow_nil: true }, on: :create # XXX should allow admins to bypass

View File

@@ -1058,6 +1058,54 @@ class Post < ApplicationRecord
none
end
def is_matches(value, current_user = User.anonymous)
case value.downcase
when "parent"
where(has_children: true)
when "child"
where.not(parent: nil)
when "sfw"
where.not(rating: ["q", "e"])
when "nsfw"
where(rating: ["q", "e"])
when *AutocompleteService::POST_STATUSES
status_matches(value, current_user)
when *MediaAsset::FILE_TYPES
attribute_matches(value, :file_ext, :enum)
when *Post::RATINGS.values.map(&:downcase)
rating_matches(value)
else
none
end
end
def has_matches(value)
case value.downcase
when "parent"
where.not(parent: nil)
when "child", "children"
where(has_children: true)
when "source"
where.not(source: "")
when "appeals"
where(id: PostAppeal.select(:post_id))
when "flags"
where(id: PostFlag.by_users.select(:post_id))
when "replacements"
where(id: PostReplacement.select(:post_id))
when "comments"
where(id: Comment.undeleted.select(:post_id))
when "commentary"
where(id: ArtistCommentary.undeleted.select(:post_id))
when "notes"
where(id: Note.active.select(:post_id))
when "pools"
where(id: Pool.undeleted.select("unnest(post_ids)"))
else
none
end
end
def status_matches(status, current_user = User.anonymous)
case status.downcase
when "pending"

View File

@@ -736,6 +736,98 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([], "duration:>1")
end
should "return posts for the is:<status> metatag" do
pending = create(:post, is_pending: true)
flagged = create(:post, is_flagged: true)
deleted = create(:post, is_deleted: true)
banned = create(:post, is_banned: true)
appealed = create(:post, is_deleted: true)
appeal = create(:post_appeal, post: appealed)
assert_tag_match([appealed, flagged, pending], "is:modqueue")
assert_tag_match([pending], "is:pending")
assert_tag_match([flagged], "is:flagged")
assert_tag_match([appealed], "is:appealed")
assert_tag_match([appealed, deleted], "is:deleted")
assert_tag_match([banned], "is:banned")
assert_tag_match([banned], "is:active")
assert_tag_match([banned], "is:active is:banned")
end
should "return posts for the is:<rating> metatag" do
s = create(:post, rating: "s")
q = create(:post, rating: "q")
e = create(:post, rating: "e")
all = [e, q, s]
assert_tag_match([s], "is:safe")
assert_tag_match([q], "is:questionable")
assert_tag_match([e], "is:explicit")
assert_tag_match([s], "is:sfw")
assert_tag_match([e, q], "is:nsfw")
end
should "return posts for the is:<filetype> metatag" do
jpg = create(:post, file_ext: "jpg")
png = create(:post, file_ext: "png")
gif = create(:post, file_ext: "gif")
mp4 = create(:post, file_ext: "mp4")
webm = create(:post, file_ext: "webm")
swf = create(:post, file_ext: "swf")
zip = create(:post, file_ext: "zip")
assert_tag_match([jpg], "is:jpg")
assert_tag_match([png], "is:png")
assert_tag_match([gif], "is:gif")
assert_tag_match([mp4], "is:mp4")
assert_tag_match([webm], "is:webm")
assert_tag_match([swf], "is:swf")
assert_tag_match([zip], "is:zip")
end
should "return posts for the is:<parent> metatag" do
parent = create(:post)
child = create(:post, parent: parent)
assert_tag_match([parent], "is:parent")
assert_tag_match([child], "is:child")
assert_tag_match([], "is:blah")
end
should "return posts for the has:<value> metatag" do
parent = create(:post)
child = create(:post, parent: parent)
appeal = create(:post_appeal)
flag = create(:post_flag)
replacement = create(:post_replacement)
comment = create(:comment)
commentary = create(:artist_commentary)
note = create(:note)
pooled = create(:post)
pool = create(:pool, post_ids: [pooled.id])
assert_tag_match([child], "has:parent")
assert_tag_match([parent], "has:child")
assert_tag_match([parent], "has:children")
assert_tag_match([appeal.post], "has:appeals")
assert_tag_match([flag.post], "has:flags")
assert_tag_match([replacement.post], "has:replacements")
assert_tag_match([comment.post], "has:comments")
assert_tag_match([commentary.post], "has:commentary")
assert_tag_match([note.post], "has:notes")
assert_tag_match([pooled], "has:pools")
assert_tag_match([], "has:blah")
end
should "return posts for the has:<source> metatag" do
post1 = create(:post, source: "blah")
post2 = create(:post, source: nil)
assert_tag_match([post1], "has:source")
end
should "return posts for the status:<type> metatag" do
pending = create(:post, is_pending: true)
flagged = create(:post, is_flagged: true)