1167 lines
37 KiB
Ruby
1167 lines
37 KiB
Ruby
class PostQueryBuilder
|
|
COUNT_METATAGS = %w[
|
|
comment_count deleted_comment_count active_comment_count
|
|
note_count deleted_note_count active_note_count
|
|
flag_count resolved_flag_count unresolved_flag_count
|
|
child_count deleted_child_count active_child_count
|
|
pool_count deleted_pool_count active_pool_count series_pool_count collection_pool_count
|
|
appeal_count approval_count replacement_count
|
|
]
|
|
|
|
# allow e.g. `deleted_comments` as a synonym for `deleted_comment_count`
|
|
COUNT_METATAG_SYNONYMS = COUNT_METATAGS.map { |str| str.delete_suffix("_count").pluralize }
|
|
|
|
METATAGS = %w[
|
|
-user user -approver approver commenter comm noter noteupdater artcomm
|
|
-pool pool ordpool -favgroup favgroup -fav fav ordfav md5 -rating rating
|
|
-locked locked width height mpixels ratio score favcount filesize source
|
|
-source id -id date age order limit -status status tagcount parent -parent
|
|
child pixiv_id pixiv search upvote downvote filetype -filetype flagger
|
|
-flagger appealer -appealer disapproved -disapproved embedded
|
|
] + TagCategory.short_name_list.map {|x| "#{x}tags"} + COUNT_METATAGS + COUNT_METATAG_SYNONYMS
|
|
|
|
ORDER_METATAGS = %w[
|
|
id id_desc
|
|
score score_asc
|
|
favcount favcount_asc
|
|
created_at created_at_asc
|
|
change change_asc
|
|
comment comment_asc
|
|
comment_bumped comment_bumped_asc
|
|
note note_asc
|
|
artcomm artcomm_asc
|
|
mpixels mpixels_asc
|
|
portrait landscape
|
|
filesize filesize_asc
|
|
tagcount tagcount_asc
|
|
rank
|
|
curated
|
|
modqueue
|
|
random
|
|
custom
|
|
] +
|
|
COUNT_METATAGS +
|
|
COUNT_METATAG_SYNONYMS.flat_map { |str| [str, "#{str}_asc"] } +
|
|
TagCategory.short_name_list.flat_map { |str| ["#{str}tags", "#{str}tags_asc"] }
|
|
|
|
attr_accessor :query_string
|
|
|
|
def initialize(query_string)
|
|
@query_string = query_string
|
|
end
|
|
|
|
def add_range_relation(arr, field, relation)
|
|
return relation if arr.nil?
|
|
|
|
case arr[0]
|
|
when :eq
|
|
if arr[1].is_a?(Time)
|
|
relation.where("#{field} between ? and ?", arr[1].beginning_of_day, arr[1].end_of_day)
|
|
else
|
|
relation.where(["#{field} = ?", arr[1]])
|
|
end
|
|
|
|
when :gt
|
|
relation.where(["#{field} > ?", arr[1]])
|
|
|
|
when :gte
|
|
relation.where(["#{field} >= ?", arr[1]])
|
|
|
|
when :lt
|
|
relation.where(["#{field} < ?", arr[1]])
|
|
|
|
when :lte
|
|
relation.where(["#{field} <= ?", arr[1]])
|
|
|
|
when :in
|
|
relation.where(["#{field} in (?)", arr[1]])
|
|
|
|
when :between
|
|
relation.where(["#{field} BETWEEN ? AND ?", arr[1], arr[2]])
|
|
|
|
else
|
|
relation
|
|
end
|
|
end
|
|
|
|
def escape_string_for_tsquery(array)
|
|
array.map(&:to_escaped_for_tsquery)
|
|
end
|
|
|
|
def add_tag_string_search_relation(tags, relation)
|
|
tag_query_sql = []
|
|
|
|
if tags[:include].any?
|
|
tag_query_sql << "(" + escape_string_for_tsquery(tags[:include]).join(" | ") + ")"
|
|
end
|
|
|
|
if tags[:related].any?
|
|
tag_query_sql << "(" + escape_string_for_tsquery(tags[:related]).join(" & ") + ")"
|
|
end
|
|
|
|
if tags[:exclude].any?
|
|
tag_query_sql << "!(" + escape_string_for_tsquery(tags[:exclude]).join(" | ") + ")"
|
|
end
|
|
|
|
if tag_query_sql.any?
|
|
relation = relation.where("posts.tag_index @@ to_tsquery('danbooru', E?)", tag_query_sql.join(" & "))
|
|
end
|
|
|
|
relation
|
|
end
|
|
|
|
def add_saved_search_relation(saved_searches, relation)
|
|
saved_searches.each do |saved_search|
|
|
if saved_search == "all"
|
|
post_ids = SavedSearch.post_ids_for(CurrentUser.id)
|
|
else
|
|
post_ids = SavedSearch.post_ids_for(CurrentUser.id, label: saved_search)
|
|
end
|
|
|
|
post_ids = [0] if post_ids.empty?
|
|
relation = relation.where("posts.id": post_ids)
|
|
end
|
|
|
|
relation
|
|
end
|
|
|
|
def table_for_metatag(metatag)
|
|
if metatag.in?(COUNT_METATAGS)
|
|
metatag[/(?<table>[a-z]+)_count\z/i, :table]
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def tables_for_query(q)
|
|
metatags = q.keys
|
|
metatags << q[:order].remove(/_(asc|desc)\z/i) if q[:order].present?
|
|
|
|
tables = metatags.map { |metatag| table_for_metatag(metatag.to_s) }
|
|
tables.compact.uniq
|
|
end
|
|
|
|
def add_joins(q, relation)
|
|
tables = tables_for_query(q)
|
|
relation = relation.with_stats(tables)
|
|
relation
|
|
end
|
|
|
|
def hide_deleted_posts?(q)
|
|
return false if CurrentUser.admin_mode?
|
|
return false if q[:status].in?(%w[deleted active any all])
|
|
return false if q[:status_neg].in?(%w[deleted active any all])
|
|
return CurrentUser.user.hide_deleted_posts?
|
|
end
|
|
|
|
def build
|
|
unless query_string.is_a?(Hash)
|
|
q = PostQueryBuilder.parse_query(query_string)
|
|
end
|
|
|
|
relation = Post.all
|
|
|
|
if q[:tag_count].to_i > Danbooru.config.tag_query_limit
|
|
raise ::Post::SearchError
|
|
end
|
|
|
|
if CurrentUser.safe_mode?
|
|
relation = relation.where("posts.rating = 's'")
|
|
end
|
|
|
|
relation = add_joins(q, relation)
|
|
relation = add_range_relation(q[:post_id], "posts.id", relation)
|
|
relation = add_range_relation(q[:mpixels], "posts.image_width * posts.image_height / 1000000.0", relation)
|
|
relation = add_range_relation(q[:ratio], "ROUND(1.0 * posts.image_width / GREATEST(1, posts.image_height), 2)", relation)
|
|
relation = add_range_relation(q[:width], "posts.image_width", relation)
|
|
relation = add_range_relation(q[:height], "posts.image_height", relation)
|
|
relation = add_range_relation(q[:score], "posts.score", relation)
|
|
relation = add_range_relation(q[:fav_count], "posts.fav_count", relation)
|
|
relation = add_range_relation(q[:filesize], "posts.file_size", relation)
|
|
relation = add_range_relation(q[:date], "posts.created_at", relation)
|
|
relation = add_range_relation(q[:age], "posts.created_at", relation)
|
|
TagCategory.categories.each do |category|
|
|
relation = add_range_relation(q["#{category}_tag_count".to_sym], "posts.tag_count_#{category}", relation)
|
|
end
|
|
relation = add_range_relation(q[:post_tag_count], "posts.tag_count", relation)
|
|
|
|
COUNT_METATAGS.each do |column|
|
|
relation = add_range_relation(q[column.to_sym], "posts.#{column}", relation)
|
|
end
|
|
|
|
if q[:md5]
|
|
relation = relation.where("posts.md5": q[:md5])
|
|
end
|
|
|
|
if q[:status] == "pending"
|
|
relation = relation.where("posts.is_pending = TRUE")
|
|
elsif q[:status] == "flagged"
|
|
relation = relation.where("posts.is_flagged = TRUE")
|
|
elsif q[:status] == "modqueue"
|
|
relation = relation.where("posts.is_pending = TRUE OR posts.is_flagged = TRUE")
|
|
elsif q[:status] == "deleted"
|
|
relation = relation.where("posts.is_deleted = TRUE")
|
|
elsif q[:status] == "banned"
|
|
relation = relation.where("posts.is_banned = TRUE")
|
|
elsif q[:status] == "active"
|
|
relation = relation.where("posts.is_pending = FALSE AND posts.is_deleted = FALSE AND posts.is_banned = FALSE AND posts.is_flagged = FALSE")
|
|
elsif q[:status] == "unmoderated"
|
|
relation = relation.merge(Post.pending_or_flagged.available_for_moderation)
|
|
elsif q[:status] == "all" || q[:status] == "any"
|
|
# do nothing
|
|
elsif q[:status_neg] == "pending"
|
|
relation = relation.where("posts.is_pending = FALSE")
|
|
elsif q[:status_neg] == "flagged"
|
|
relation = relation.where("posts.is_flagged = FALSE")
|
|
elsif q[:status_neg] == "modqueue"
|
|
relation = relation.where("posts.is_pending = FALSE AND posts.is_flagged = FALSE")
|
|
elsif q[:status_neg] == "deleted"
|
|
relation = relation.where("posts.is_deleted = FALSE")
|
|
elsif q[:status_neg] == "banned"
|
|
relation = relation.where("posts.is_banned = FALSE")
|
|
elsif q[:status_neg] == "active"
|
|
relation = relation.where("posts.is_pending = TRUE OR posts.is_deleted = TRUE OR posts.is_banned = TRUE OR posts.is_flagged = TRUE")
|
|
end
|
|
|
|
if hide_deleted_posts?(q)
|
|
relation = relation.where("posts.is_deleted = FALSE")
|
|
end
|
|
|
|
if q[:filetype]
|
|
relation = relation.where("posts.file_ext": q[:filetype])
|
|
end
|
|
|
|
if q[:filetype_neg]
|
|
relation = relation.where.not("posts.file_ext": q[:filetype_neg])
|
|
end
|
|
|
|
if q[:source]
|
|
if q[:source] == "none"
|
|
relation = relation.where_like(:source, '')
|
|
else
|
|
relation = relation.where_ilike(:source, q[:source].downcase + "*")
|
|
end
|
|
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
|
|
end
|
|
|
|
q[:pool].to_a.each do |pool_name|
|
|
case pool_name
|
|
when "none"
|
|
relation = relation.where.not(id: Pool.select("unnest(post_ids)"))
|
|
when "any"
|
|
relation = relation.where(id: Pool.select("unnest(post_ids)"))
|
|
when "series"
|
|
relation = relation.where(id: Pool.series.select("unnest(post_ids)"))
|
|
when "collection"
|
|
relation = relation.where(id: Pool.collection.select("unnest(post_ids)"))
|
|
when /\*/
|
|
relation = relation.where(id: Pool.name_matches(pool_name).select("unnest(post_ids)"))
|
|
else
|
|
relation = relation.where(id: Pool.named(pool_name).select("unnest(post_ids)"))
|
|
end
|
|
end
|
|
|
|
q[:pool_neg].to_a.each do |pool_name|
|
|
case pool_name
|
|
when "none"
|
|
relation = relation.where(id: Pool.select("unnest(post_ids)"))
|
|
when "any"
|
|
relation = relation.where.not(id: Pool.select("unnest(post_ids)"))
|
|
when "series"
|
|
relation = relation.where.not(id: Pool.series.select("unnest(post_ids)"))
|
|
when "collection"
|
|
relation = relation.where.not(id: Pool.collection.select("unnest(post_ids)"))
|
|
when /\*/
|
|
relation = relation.where.not(id: Pool.name_matches(pool_name).select("unnest(post_ids)"))
|
|
else
|
|
relation = relation.where.not(id: Pool.named(pool_name).select("unnest(post_ids)"))
|
|
end
|
|
end
|
|
|
|
if q[:saved_searches]
|
|
relation = add_saved_search_relation(q[:saved_searches], relation)
|
|
end
|
|
|
|
if q[:uploader_id_neg]
|
|
relation = relation.where.not("posts.uploader_id": q[:uploader_id_neg])
|
|
end
|
|
|
|
if q[:uploader_id]
|
|
relation = relation.where("posts.uploader_id": q[:uploader_id])
|
|
end
|
|
|
|
if q[:approver_id_neg]
|
|
relation = relation.where.not("posts.approver_id": q[:approver_id_neg])
|
|
end
|
|
|
|
if q[:approver_id]
|
|
if q[:approver_id] == "any"
|
|
relation = relation.where("posts.approver_id is not null")
|
|
elsif q[:approver_id] == "none"
|
|
relation = relation.where("posts.approver_id is null")
|
|
else
|
|
relation = relation.where("posts.approver_id": q[:approver_id])
|
|
end
|
|
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
|
|
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
|
|
end
|
|
|
|
q[:flaggers_neg].to_a.each do |flagger|
|
|
flags = PostFlag.unscoped.creator_matches(flagger, CurrentUser.user).where("post_id = posts.id").select("1")
|
|
relation = relation.where("NOT EXISTS (#{flags.to_sql})")
|
|
end
|
|
|
|
q[:flaggers].to_a.each do |flagger|
|
|
if flagger == "any"
|
|
flags = PostFlag.unscoped.category_matches("normal").where("post_id = posts.id").select("1")
|
|
relation = relation.where("EXISTS (#{flags.to_sql})")
|
|
elsif flagger == "none"
|
|
flags = PostFlag.unscoped.category_matches("normal").where("post_id = posts.id").select("1")
|
|
relation = relation.where("NOT EXISTS (#{flags.to_sql})")
|
|
else
|
|
flags = PostFlag.unscoped.creator_matches(flagger, CurrentUser.user).where("post_id = posts.id").select("1")
|
|
relation = relation.where("EXISTS (#{flags.to_sql})")
|
|
end
|
|
end
|
|
|
|
if q[:appealer_ids_neg]
|
|
q[:appealer_ids_neg].each do |appealer_id|
|
|
relation = relation.where.not("posts.id": PostAppeal.unscoped.where(creator_id: appealer_id).select(:post_id).distinct)
|
|
end
|
|
end
|
|
|
|
if q[:appealer_ids]
|
|
q[:appealer_ids].each do |appealer_id|
|
|
if appealer_id == "any"
|
|
relation = relation.where('EXISTS (' + PostAppeal.unscoped.where('post_id = posts.id').select('1').to_sql + ')')
|
|
elsif appealer_id == "none"
|
|
relation = relation.where('NOT EXISTS (' + PostAppeal.unscoped.where('post_id = posts.id').select('1').to_sql + ')')
|
|
else
|
|
relation = relation.where("posts.id": PostAppeal.unscoped.where(creator_id: appealer_id).select(:post_id).distinct)
|
|
end
|
|
end
|
|
end
|
|
|
|
if q[:commenter_ids]
|
|
q[:commenter_ids].each do |commenter_id|
|
|
if commenter_id == "any"
|
|
relation = relation.where("posts.last_commented_at is not null")
|
|
elsif commenter_id == "none"
|
|
relation = relation.where("posts.last_commented_at is null")
|
|
else
|
|
relation = relation.where("posts.id": Comment.unscoped.where(creator_id: commenter_id).select(:post_id).distinct)
|
|
end
|
|
end
|
|
end
|
|
|
|
if q[:noter_ids]
|
|
q[:noter_ids].each do |noter_id|
|
|
if noter_id == "any"
|
|
relation = relation.where("posts.last_noted_at is not null")
|
|
elsif noter_id == "none"
|
|
relation = relation.where("posts.last_noted_at is null")
|
|
else
|
|
relation = relation.where("posts.id": NoteVersion.unscoped.where(version: 1, updater_id: noter_id).select(:post_id).distinct)
|
|
end
|
|
end
|
|
end
|
|
|
|
if q[:note_updater_ids]
|
|
q[:note_updater_ids].each do |note_updater_id|
|
|
relation = relation.where("posts.id": NoteVersion.unscoped.where(updater_id: note_updater_id).select("post_id").distinct)
|
|
end
|
|
end
|
|
|
|
if q[:artcomm_ids]
|
|
q[:artcomm_ids].each do |artcomm_id|
|
|
relation = relation.where("posts.id": ArtistCommentaryVersion.unscoped.where(updater_id: artcomm_id).select("post_id").distinct)
|
|
end
|
|
end
|
|
|
|
if q[:post_id_negated]
|
|
relation = relation.where("posts.id <> ?", q[:post_id_negated])
|
|
end
|
|
|
|
if q[:parent] == "none"
|
|
relation = relation.where("posts.parent_id IS NULL")
|
|
elsif q[:parent] == "any"
|
|
relation = relation.where("posts.parent_id IS NOT NULL")
|
|
elsif q[:parent]
|
|
relation = relation.where("(posts.id = ? or posts.parent_id = ?)", q[:parent].to_i, q[:parent].to_i)
|
|
end
|
|
|
|
if q[:parent_neg_ids]
|
|
neg_ids = q[:parent_neg_ids].map(&:to_i)
|
|
neg_ids.delete(0)
|
|
if neg_ids.present?
|
|
relation = relation.where("posts.id not in (?) and (posts.parent_id is null or posts.parent_id not in (?))", neg_ids, neg_ids)
|
|
end
|
|
end
|
|
|
|
if q[:child] == "none"
|
|
relation = relation.where("posts.has_children = FALSE")
|
|
elsif q[:child] == "any"
|
|
relation = relation.where("posts.has_children = TRUE")
|
|
end
|
|
|
|
if q[:pixiv_id]
|
|
if q[:pixiv_id] == "any"
|
|
relation = relation.where("posts.pixiv_id IS NOT NULL")
|
|
elsif q[:pixiv_id] == "none"
|
|
relation = relation.where("posts.pixiv_id IS NULL")
|
|
else
|
|
relation = add_range_relation(q[:pixiv_id], "posts.pixiv_id", relation)
|
|
end
|
|
end
|
|
|
|
if q[:rating] =~ /^q/
|
|
relation = relation.where("posts.rating = 'q'")
|
|
elsif q[:rating] =~ /^s/
|
|
relation = relation.where("posts.rating = 's'")
|
|
elsif q[:rating] =~ /^e/
|
|
relation = relation.where("posts.rating = 'e'")
|
|
end
|
|
|
|
if q[:rating_negated] =~ /^q/
|
|
relation = relation.where("posts.rating <> 'q'")
|
|
elsif q[:rating_negated] =~ /^s/
|
|
relation = relation.where("posts.rating <> 's'")
|
|
elsif q[:rating_negated] =~ /^e/
|
|
relation = relation.where("posts.rating <> 'e'")
|
|
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")
|
|
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")
|
|
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)
|
|
end
|
|
|
|
relation = add_tag_string_search_relation(q[:tags], relation)
|
|
|
|
if q[:ordpool].present?
|
|
pool_name = q[:ordpool]
|
|
|
|
# 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")
|
|
end
|
|
|
|
q[:favgroups_neg].to_a.each do |favgroup|
|
|
relation = relation.where.not(id: FavoriteGroup.where(id: favgroup.id).select("unnest(post_ids)"))
|
|
end
|
|
|
|
q[:favgroups].to_a.each do |favgroup|
|
|
relation = relation.where(id: FavoriteGroup.where(id: favgroup.id).select("unnest(post_ids)"))
|
|
end
|
|
|
|
q[:upvoter].to_a.each do |voter|
|
|
votes = PostVote.positive.visible(CurrentUser.user).where(user: voter).where("post_id = posts.id").select("1")
|
|
relation = relation.where("EXISTS (#{votes.to_sql})")
|
|
end
|
|
|
|
q[:downvoter].to_a.each do |voter|
|
|
votes = PostVote.negative.visible(CurrentUser.user).where(user: voter).where("post_id = posts.id").select("1")
|
|
relation = relation.where("EXISTS (#{votes.to_sql})")
|
|
end
|
|
|
|
if q[:ordfav].present?
|
|
user_id = q[:ordfav].to_i
|
|
relation = relation.joins("INNER JOIN favorites ON favorites.post_id = posts.id")
|
|
relation = relation.where("favorites.user_id % 100 = ? and favorites.user_id = ?", user_id % 100, user_id).order("favorites.id DESC")
|
|
end
|
|
|
|
# HACK: if we're using a date: or age: metatag, default to ordering by
|
|
# created_at instead of id so that the query will use the created_at index.
|
|
if q[:date].present? || q[:age].present?
|
|
case q[:order]
|
|
when "id", "id_asc"
|
|
q[:order] = "created_at_asc"
|
|
when "id_desc", nil
|
|
q[:order] = "created_at_desc"
|
|
end
|
|
end
|
|
|
|
if q[:order] == "rank"
|
|
relation = relation.where("posts.score > 0 and posts.created_at >= ?", 2.days.ago)
|
|
elsif q[:order] == "landscape" || q[:order] == "portrait"
|
|
relation = relation.where("posts.image_width IS NOT NULL and posts.image_height IS NOT NULL")
|
|
end
|
|
|
|
if q[:order] == "custom" && q[:post_id].present? && q[:post_id][0] == :in
|
|
relation = relation.find_ordered(q[:post_id][1])
|
|
else
|
|
relation = PostQueryBuilder.search_order(relation, q[:order])
|
|
end
|
|
|
|
relation
|
|
end
|
|
|
|
def self.search_order(relation, order)
|
|
case order.to_s
|
|
when "id", "id_asc"
|
|
relation = relation.order("posts.id ASC")
|
|
|
|
when "id_desc"
|
|
relation = relation.order("posts.id DESC")
|
|
|
|
when "score", "score_desc"
|
|
relation = relation.order("posts.score DESC, posts.id DESC")
|
|
|
|
when "score_asc"
|
|
relation = relation.order("posts.score ASC, posts.id ASC")
|
|
|
|
when "favcount"
|
|
relation = relation.order("posts.fav_count DESC, posts.id DESC")
|
|
|
|
when "favcount_asc"
|
|
relation = relation.order("posts.fav_count ASC, posts.id ASC")
|
|
|
|
when "created_at", "created_at_desc"
|
|
relation = relation.order("posts.created_at DESC")
|
|
|
|
when "created_at_asc"
|
|
relation = relation.order("posts.created_at ASC")
|
|
|
|
when "change", "change_desc"
|
|
relation = relation.order("posts.updated_at DESC, posts.id DESC")
|
|
|
|
when "change_asc"
|
|
relation = relation.order("posts.updated_at ASC, posts.id ASC")
|
|
|
|
when "comment", "comm"
|
|
relation = relation.order("posts.last_commented_at DESC NULLS LAST, posts.id DESC")
|
|
|
|
when "comment_asc", "comm_asc"
|
|
relation = relation.order("posts.last_commented_at ASC NULLS LAST, posts.id ASC")
|
|
|
|
when "comment_bumped"
|
|
relation = relation.order("posts.last_comment_bumped_at DESC NULLS LAST")
|
|
|
|
when "comment_bumped_asc"
|
|
relation = relation.order("posts.last_comment_bumped_at ASC NULLS FIRST")
|
|
|
|
when "note"
|
|
relation = relation.order("posts.last_noted_at DESC NULLS LAST")
|
|
|
|
when "note_asc"
|
|
relation = relation.order("posts.last_noted_at ASC NULLS FIRST")
|
|
|
|
when "artcomm"
|
|
relation = relation.joins("INNER JOIN artist_commentaries ON artist_commentaries.post_id = posts.id")
|
|
relation = relation.order("artist_commentaries.updated_at DESC")
|
|
|
|
when "artcomm_asc"
|
|
relation = relation.joins("INNER JOIN artist_commentaries ON artist_commentaries.post_id = posts.id")
|
|
relation = relation.order("artist_commentaries.updated_at ASC")
|
|
|
|
when "mpixels", "mpixels_desc"
|
|
relation = relation.where(Arel.sql("posts.image_width is not null and posts.image_height is not null"))
|
|
# Use "w*h/1000000", even though "w*h" would give the same result, so this can use
|
|
# the posts_mpixels index.
|
|
relation = relation.order(Arel.sql("posts.image_width * posts.image_height / 1000000.0 DESC"))
|
|
|
|
when "mpixels_asc"
|
|
relation = relation.where("posts.image_width is not null and posts.image_height is not null")
|
|
relation = relation.order(Arel.sql("posts.image_width * posts.image_height / 1000000.0 ASC"))
|
|
|
|
when "portrait"
|
|
relation = relation.order(Arel.sql("1.0 * posts.image_width / GREATEST(1, posts.image_height) ASC"))
|
|
|
|
when "landscape"
|
|
relation = relation.order(Arel.sql("1.0 * posts.image_width / GREATEST(1, posts.image_height) DESC"))
|
|
|
|
when "filesize", "filesize_desc"
|
|
relation = relation.order("posts.file_size DESC")
|
|
|
|
when "filesize_asc"
|
|
relation = relation.order("posts.file_size ASC")
|
|
|
|
when /\A(?<column>#{COUNT_METATAGS.join("|")})(_(?<direction>asc|desc))?\z/i
|
|
column = $~[:column]
|
|
direction = $~[:direction] || "desc"
|
|
relation = relation.order(column => direction, :id => direction)
|
|
|
|
when "tagcount", "tagcount_desc"
|
|
relation = relation.order("posts.tag_count DESC")
|
|
|
|
when "tagcount_asc"
|
|
relation = relation.order("posts.tag_count ASC")
|
|
|
|
when /(#{TagCategory.short_name_regex})tags(?:\Z|_desc)/
|
|
relation = relation.order("posts.tag_count_#{TagCategory.short_name_mapping[$1]} DESC")
|
|
|
|
when /(#{TagCategory.short_name_regex})tags_asc/
|
|
relation = relation.order("posts.tag_count_#{TagCategory.short_name_mapping[$1]} ASC")
|
|
|
|
when "rank"
|
|
relation = relation.order(Arel.sql("log(3, posts.score) + (extract(epoch from posts.created_at) - extract(epoch from timestamp '2005-05-24')) / 35000 DESC"))
|
|
|
|
when "curated"
|
|
contributors = User.bit_prefs_match(:can_upload_free, true)
|
|
|
|
relation = relation
|
|
.joins(:favorites)
|
|
.where(favorites: { user: contributors })
|
|
.group("posts.id")
|
|
.select("posts.*, COUNT(*) AS contributor_fav_count")
|
|
.order("contributor_fav_count DESC, posts.fav_count DESC, posts.id DESC")
|
|
|
|
when "modqueue", "modqueue_desc"
|
|
relation = relation.left_outer_joins(:flags).order(Arel.sql("GREATEST(posts.created_at, post_flags.created_at) DESC, posts.id DESC"))
|
|
|
|
when "modqueue_asc"
|
|
relation = relation.left_outer_joins(:flags).order(Arel.sql("GREATEST(posts.created_at, post_flags.created_at) ASC, posts.id ASC"))
|
|
|
|
else
|
|
relation = relation.order("posts.id DESC")
|
|
end
|
|
|
|
relation
|
|
end
|
|
|
|
concerning :ParseMethods do
|
|
class_methods do
|
|
def scan_query(query, strip_metatags: false)
|
|
tagstr = query.to_s.gsub(/\u3000/, " ").strip
|
|
list = tagstr.scan(/-?source:".*?"/) || []
|
|
list += tagstr.gsub(/-?source:".*?"/, "").scan(/[^[:space:]]+/).uniq
|
|
list = list.map { |tag| tag.sub(/^[-~]/, "") } if strip_metatags
|
|
list
|
|
end
|
|
|
|
def normalize_query(query, normalize_aliases: true, sort: true)
|
|
tags = scan_query(query.to_s)
|
|
tags = tags.map { |t| Tag.normalize_name(t) }
|
|
tags = TagAlias.to_aliased(tags) if normalize_aliases
|
|
tags = tags.sort if sort
|
|
tags = tags.uniq
|
|
tags.join(" ")
|
|
end
|
|
|
|
def parse_query(query, options = {})
|
|
q = {}
|
|
|
|
q[:tag_count] = 0
|
|
|
|
q[:tags] = {
|
|
:related => [],
|
|
:include => [],
|
|
:exclude => []
|
|
}
|
|
|
|
scan_query(query).each do |token|
|
|
q[:tag_count] += 1 unless Danbooru.config.is_unlimited_tag?(token)
|
|
|
|
if token =~ /\A(#{METATAGS.join("|")}):(.+)\z/i
|
|
g1 = $1.downcase
|
|
g2 = $2
|
|
case g1
|
|
when "-user"
|
|
q[:uploader_id_neg] ||= []
|
|
user_id = User.name_to_id(g2)
|
|
q[:uploader_id_neg] << user_id unless user_id.blank?
|
|
|
|
when "user"
|
|
user_id = User.name_to_id(g2)
|
|
q[:uploader_id] = user_id unless user_id.blank?
|
|
|
|
when "-approver"
|
|
if g2 == "none"
|
|
q[:approver_id] = "any"
|
|
elsif g2 == "any"
|
|
q[:approver_id] = "none"
|
|
else
|
|
q[:approver_id_neg] ||= []
|
|
user_id = User.name_to_id(g2)
|
|
q[:approver_id_neg] << user_id unless user_id.blank?
|
|
end
|
|
|
|
when "approver"
|
|
if g2 == "none"
|
|
q[:approver_id] = "none"
|
|
elsif g2 == "any"
|
|
q[:approver_id] = "any"
|
|
else
|
|
user_id = User.name_to_id(g2)
|
|
q[:approver_id] = user_id unless user_id.blank?
|
|
end
|
|
|
|
when "flagger"
|
|
if g2 == "none"
|
|
q[:flaggers] ||= []
|
|
q[:flaggers] << "none"
|
|
elsif g2 == "any"
|
|
q[:flaggers] ||= []
|
|
q[:flaggers] << "any"
|
|
else
|
|
user = User.find_by_name(g2)
|
|
q[:flaggers] ||= []
|
|
q[:flaggers] << user unless user.blank?
|
|
end
|
|
|
|
when "-flagger"
|
|
if g2 == "none"
|
|
q[:flaggers] ||= []
|
|
q[:flaggers] << "any"
|
|
elsif g2 == "any"
|
|
q[:flaggers] ||= []
|
|
q[:flaggers] << "none"
|
|
else
|
|
user = User.find_by_name(g2)
|
|
q[:flaggers_neg] ||= []
|
|
q[:flaggers_neg] << user unless user.blank?
|
|
end
|
|
|
|
when "appealer"
|
|
q[:appealer_ids] ||= []
|
|
|
|
if g2 == "none"
|
|
q[:appealer_ids] << "none"
|
|
elsif g2 == "any"
|
|
q[:appealer_ids] << "any"
|
|
else
|
|
user_id = User.name_to_id(g2)
|
|
q[:appealer_ids] << user_id unless user_id.blank?
|
|
end
|
|
|
|
when "-appealer"
|
|
if g2 == "none"
|
|
q[:appealer_ids] ||= []
|
|
q[:appealer_ids] << "any"
|
|
elsif g2 == "any"
|
|
q[:appealer_ids] ||= []
|
|
q[:appealer_ids] << "none"
|
|
else
|
|
q[:appealer_ids_neg] ||= []
|
|
user_id = User.name_to_id(g2)
|
|
q[:appealer_ids_neg] << user_id unless user_id.blank?
|
|
end
|
|
|
|
when "commenter", "comm"
|
|
q[:commenter_ids] ||= []
|
|
|
|
if g2 == "none"
|
|
q[:commenter_ids] << "none"
|
|
elsif g2 == "any"
|
|
q[:commenter_ids] << "any"
|
|
else
|
|
user_id = User.name_to_id(g2)
|
|
q[:commenter_ids] << user_id unless user_id.blank?
|
|
end
|
|
|
|
when "noter"
|
|
q[:noter_ids] ||= []
|
|
|
|
if g2 == "none"
|
|
q[:noter_ids] << "none"
|
|
elsif g2 == "any"
|
|
q[:noter_ids] << "any"
|
|
else
|
|
user_id = User.name_to_id(g2)
|
|
q[:noter_ids] << user_id unless user_id.blank?
|
|
end
|
|
|
|
when "noteupdater"
|
|
q[:note_updater_ids] ||= []
|
|
user_id = User.name_to_id(g2)
|
|
q[:note_updater_ids] << user_id unless user_id.blank?
|
|
|
|
when "artcomm"
|
|
q[:artcomm_ids] ||= []
|
|
user_id = User.name_to_id(g2)
|
|
q[:artcomm_ids] << user_id unless user_id.blank?
|
|
|
|
when "disapproved"
|
|
q[:disapproved] ||= []
|
|
q[:disapproved] << g2
|
|
|
|
when "-disapproved"
|
|
q[:disapproved_neg] ||= []
|
|
q[:disapproved_neg] << g2
|
|
|
|
when "-pool"
|
|
q[:pool_neg] ||= []
|
|
q[:pool_neg] << g2
|
|
|
|
when "pool"
|
|
q[:pool] ||= []
|
|
q[:pool] << g2
|
|
|
|
when "ordpool"
|
|
q[:ordpool] = g2
|
|
|
|
when "-favgroup"
|
|
favgroup = FavoriteGroup.find_by_name_or_id!(g2, CurrentUser.user)
|
|
raise User::PrivilegeError unless Pundit.policy!([CurrentUser.user, nil], favgroup).show?
|
|
|
|
q[:favgroups_neg] ||= []
|
|
q[:favgroups_neg] << favgroup
|
|
|
|
when "favgroup"
|
|
favgroup = FavoriteGroup.find_by_name_or_id!(g2, CurrentUser.user)
|
|
raise User::PrivilegeError unless Pundit.policy!([CurrentUser.user, nil], favgroup).show?
|
|
|
|
q[:favgroups] ||= []
|
|
q[:favgroups] << favgroup
|
|
|
|
when "-fav"
|
|
favuser = User.find_by_name(g2)
|
|
|
|
if favuser.nil? || !Pundit.policy!([CurrentUser.user, nil], favuser).can_see_favorites?
|
|
raise User::PrivilegeError
|
|
end
|
|
|
|
q[:tags][:exclude] << "fav:#{favuser.id}"
|
|
|
|
when "fav"
|
|
favuser = User.find_by_name(g2)
|
|
|
|
if favuser.nil? || !Pundit.policy!([CurrentUser.user, nil], favuser).can_see_favorites?
|
|
raise User::PrivilegeError
|
|
end
|
|
|
|
q[:tags][:related] << "fav:#{favuser.id}"
|
|
|
|
when "ordfav"
|
|
favuser = User.find_by_name(g2)
|
|
|
|
if favuser.nil? || !Pundit.policy!([CurrentUser.user, nil], favuser).can_see_favorites?
|
|
raise User::PrivilegeError.new
|
|
end
|
|
|
|
q[:tags][:related] << "fav:#{favuser.id}"
|
|
q[:ordfav] = favuser.id
|
|
|
|
when "search"
|
|
q[:saved_searches] ||= []
|
|
q[:saved_searches] << g2
|
|
|
|
when "md5"
|
|
q[:md5] = g2.downcase.split(/,/)
|
|
|
|
when "-rating"
|
|
q[:rating_negated] = g2.downcase
|
|
|
|
when "rating"
|
|
q[:rating] = g2.downcase
|
|
|
|
when "-locked"
|
|
q[:locked_negated] = g2.downcase
|
|
|
|
when "locked"
|
|
q[:locked] = g2.downcase
|
|
|
|
when "id"
|
|
q[:post_id] = parse_helper(g2)
|
|
|
|
when "-id"
|
|
q[:post_id_negated] = g2.to_i
|
|
|
|
when "width"
|
|
q[:width] = parse_helper(g2)
|
|
|
|
when "height"
|
|
q[:height] = parse_helper(g2)
|
|
|
|
when "mpixels"
|
|
q[:mpixels] = parse_helper_fudged(g2, :float)
|
|
|
|
when "ratio"
|
|
q[:ratio] = parse_helper(g2, :ratio)
|
|
|
|
when "score"
|
|
q[:score] = parse_helper(g2)
|
|
|
|
when "favcount"
|
|
q[:fav_count] = parse_helper(g2)
|
|
|
|
when "filesize"
|
|
q[:filesize] = parse_helper_fudged(g2, :filesize)
|
|
|
|
when "source"
|
|
q[:source] = g2.gsub(/\A"(.*)"\Z/, '\1')
|
|
|
|
when "-source"
|
|
q[:source_neg] = g2.gsub(/\A"(.*)"\Z/, '\1')
|
|
|
|
when "date"
|
|
q[:date] = parse_helper(g2, :date)
|
|
|
|
when "age"
|
|
q[:age] = reverse_parse_helper(parse_helper(g2, :age))
|
|
|
|
when "tagcount"
|
|
q[:post_tag_count] = parse_helper(g2)
|
|
|
|
when /(#{TagCategory.short_name_regex})tags/
|
|
q["#{TagCategory.short_name_mapping[$1]}_tag_count".to_sym] = parse_helper(g2)
|
|
|
|
when "parent"
|
|
q[:parent] = g2.downcase
|
|
|
|
when "-parent"
|
|
if g2.downcase == "none"
|
|
q[:parent] = "any"
|
|
elsif g2.downcase == "any"
|
|
q[:parent] = "none"
|
|
else
|
|
q[:parent_neg_ids] ||= []
|
|
q[:parent_neg_ids] << g2.downcase
|
|
end
|
|
|
|
when "child"
|
|
q[:child] = g2.downcase
|
|
|
|
when "order"
|
|
g2 = g2.downcase
|
|
|
|
order, suffix, _tail = g2.partition(/_(asc|desc)\z/i)
|
|
if order.in?(COUNT_METATAG_SYNONYMS)
|
|
g2 = order.singularize + "_count" + suffix
|
|
end
|
|
|
|
q[:order] = g2
|
|
|
|
when "limit"
|
|
# Do nothing. The controller takes care of it.
|
|
|
|
when "-status"
|
|
q[:status_neg] = g2.downcase
|
|
|
|
when "status"
|
|
q[:status] = g2.downcase
|
|
|
|
when "embedded"
|
|
q[:embedded] = g2.downcase
|
|
|
|
when "filetype"
|
|
q[:filetype] = g2.downcase
|
|
|
|
when "-filetype"
|
|
q[:filetype_neg] = g2.downcase
|
|
|
|
when "pixiv_id", "pixiv"
|
|
if g2.downcase == "any" || g2.downcase == "none"
|
|
q[:pixiv_id] = g2.downcase
|
|
else
|
|
q[:pixiv_id] = parse_helper(g2)
|
|
end
|
|
|
|
when "upvote"
|
|
user = User.find_by_name(g2)
|
|
q[:upvoter] ||= []
|
|
q[:upvoter] << user unless user.blank?
|
|
|
|
when "downvote"
|
|
user = User.find_by_name(g2)
|
|
q[:downvoter] ||= []
|
|
q[:downvoter] << user unless user.blank?
|
|
|
|
when *COUNT_METATAGS
|
|
q[g1.to_sym] = parse_helper(g2)
|
|
|
|
when *COUNT_METATAG_SYNONYMS
|
|
g1 = "#{g1.singularize}_count"
|
|
q[g1.to_sym] = parse_helper(g2)
|
|
|
|
end
|
|
|
|
else
|
|
parse_tag(token, q[:tags])
|
|
end
|
|
end
|
|
|
|
q[:tags][:exclude] = TagAlias.to_aliased(q[:tags][:exclude])
|
|
q[:tags][:include] = TagAlias.to_aliased(q[:tags][:include])
|
|
q[:tags][:related] = TagAlias.to_aliased(q[:tags][:related])
|
|
|
|
return q
|
|
end
|
|
|
|
def parse_tag_operator(tag)
|
|
tag = Tag.normalize_name(tag)
|
|
|
|
if tag.starts_with?("-")
|
|
["-", tag.delete_prefix("-")]
|
|
elsif tag.starts_with?("~")
|
|
["~", tag.delete_prefix("~")]
|
|
else
|
|
[nil, tag]
|
|
end
|
|
end
|
|
|
|
def parse_tag(tag, output)
|
|
operator, tag = parse_tag_operator(tag)
|
|
|
|
if tag.include?("*")
|
|
tags = Tag.wildcard_matches(tag)
|
|
|
|
if operator == "-"
|
|
output[:exclude] += tags
|
|
else
|
|
tags = ["~no_matches~"] if tags.empty? # force empty results if wildcard found no matches.
|
|
output[:include] += tags
|
|
end
|
|
elsif operator == "-"
|
|
output[:exclude] << tag
|
|
elsif operator == "~"
|
|
output[:include] << tag
|
|
else
|
|
output[:related] << tag
|
|
end
|
|
end
|
|
|
|
def parse_cast(object, type)
|
|
case type
|
|
when :integer
|
|
object.to_i
|
|
|
|
when :float
|
|
object.to_f
|
|
|
|
when :date, :datetime
|
|
Time.zone.parse(object) rescue nil
|
|
|
|
when :age
|
|
DurationParser.parse(object).ago
|
|
|
|
when :ratio
|
|
object =~ /\A(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)\Z/i
|
|
|
|
if $1 && $2.to_f != 0.0
|
|
($1.to_f / $2.to_f).round(2)
|
|
else
|
|
object.to_f.round(2)
|
|
end
|
|
|
|
when :filesize
|
|
object =~ /\A(\d+(?:\.\d*)?|\d*\.\d+)([kKmM]?)[bB]?\Z/
|
|
|
|
size = $1.to_f
|
|
unit = $2
|
|
|
|
conversion_factor = case unit
|
|
when /m/i
|
|
1024 * 1024
|
|
when /k/i
|
|
1024
|
|
else
|
|
1
|
|
end
|
|
|
|
(size * conversion_factor).to_i
|
|
end
|
|
end
|
|
|
|
def parse_helper(range, type = :integer)
|
|
# "1", "0.5", "5.", ".5":
|
|
# (-?(\d+(\.\d*)?|\d*\.\d+))
|
|
case range
|
|
when /\A(.+?)\.\.(.+)/
|
|
return [:between, parse_cast($1, type), parse_cast($2, type)]
|
|
|
|
when /\A<=(.+)/, /\A\.\.(.+)/
|
|
return [:lte, parse_cast($1, type)]
|
|
|
|
when /\A<(.+)/
|
|
return [:lt, parse_cast($1, type)]
|
|
|
|
when /\A>=(.+)/, /\A(.+)\.\.\Z/
|
|
return [:gte, parse_cast($1, type)]
|
|
|
|
when /\A>(.+)/
|
|
return [:gt, parse_cast($1, type)]
|
|
|
|
when /[, ]/
|
|
return [:in, range.split(/[, ]+/).map {|x| parse_cast(x, type)}]
|
|
|
|
else
|
|
return [:eq, parse_cast(range, type)]
|
|
|
|
end
|
|
end
|
|
|
|
def parse_helper_fudged(range, type)
|
|
result = parse_helper(range, type)
|
|
# Don't fudge the filesize when searching filesize:123b or filesize:123.
|
|
if result[0] == :eq && type == :filesize && range !~ /[km]b?\Z/i
|
|
result
|
|
elsif result[0] == :eq
|
|
new_min = (result[1] * 0.95).to_i
|
|
new_max = (result[1] * 1.05).to_i
|
|
[:between, new_min, new_max]
|
|
else
|
|
result
|
|
end
|
|
end
|
|
|
|
def reverse_parse_helper(array)
|
|
case array[0]
|
|
when :between
|
|
[:between, *array[1..-1].reverse]
|
|
|
|
when :lte
|
|
[:gte, *array[1..-1]]
|
|
|
|
when :lt
|
|
[:gt, *array[1..-1]]
|
|
|
|
when :gte
|
|
[:lte, *array[1..-1]]
|
|
|
|
when :gt
|
|
[:lt, *array[1..-1]]
|
|
|
|
else
|
|
array
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|