diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb index d9e5358b3..af5918837 100644 --- a/app/logical/autocomplete_service.rb +++ b/app/logical/autocomplete_service.rb @@ -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 diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index b7060b6c0..2e8c747a4 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -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" diff --git a/app/models/media_asset.rb b/app/models/media_asset.rb index 457c9c7e0..8f77b1d94 100644 --- a/app/models/media_asset.rb +++ b/app/models/media_asset.rb @@ -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 diff --git a/app/models/post.rb b/app/models/post.rb index 3f3f2b7f2..bf2d5869b 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -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" diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index 2d488e5f9..c25350a8d 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -736,6 +736,98 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([], "duration:>1") end + should "return posts for the is: 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: 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: 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: 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: 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: 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: metatag" do pending = create(:post, is_pending: true) flagged = create(:post, is_flagged: true)