Stop using the pool_string attribute on posts: * Stop updating it when adding or removing posts from pools. * Stop returning pool_string in the /posts.json API. * Stop including the `data-pools` attribute on thumbnails. The pool_string attribute was used in the past to facilitate pool:X searches. Posts had a hidden pool_string attribute that contained a list of every pool the post belonged to. These pools were treated like fake hidden tags on the post and a search for `pool:X` was treated like a tag search. The pool_string has no longer been used for this purpose for a long time now, and was only maintained for API compatibility purposes. Getting rid of it eliminates a bunch of legacy cruft relating to adding and removing posts from pools. If you need to see which pools a post belongs to, do this: * https://danbooru.donmai.us/pools.json?search[post_ids_include_any]=318550 The `data-pools` attribute on thumbnails was used by some people to add custom borders to pooled posts with custom CSS. This will no longer work. This was already broken because it included things like collection pools and deleted pools, which you probably didn't want. Use a userscript to add this attribute back to thumbnails if you need it.
247 lines
6.1 KiB
Ruby
247 lines
6.1 KiB
Ruby
class Pool < ApplicationRecord
|
|
class RevertError < StandardError; end
|
|
POOL_ORDER_LIMIT = 1000
|
|
|
|
array_attribute :post_ids, parse: /\d+/, cast: :to_i
|
|
|
|
validates :name, uniqueness: { case_sensitive: false }, if: :name_changed?
|
|
validate :validate_name, if: :name_changed?
|
|
validates :category, inclusion: { in: %w[series collection] }
|
|
validate :updater_can_edit_deleted
|
|
before_validation :normalize_post_ids
|
|
before_validation :normalize_name
|
|
after_save :create_version
|
|
|
|
deletable
|
|
|
|
scope :series, -> { where(category: "series") }
|
|
scope :collection, -> { where(category: "collection") }
|
|
|
|
module SearchMethods
|
|
def name_matches(name)
|
|
name = normalize_name_for_search(name)
|
|
name = "*#{name}*" unless name =~ /\*/
|
|
where_ilike(:name, name)
|
|
end
|
|
|
|
def post_tags_match(query)
|
|
posts = Post.user_tag_match(query).select(:id).reorder(nil)
|
|
pools = Pool.joins("CROSS JOIN unnest(post_ids) AS post_id").group(:id).where("post_id IN (?)", posts)
|
|
where(id: pools)
|
|
end
|
|
|
|
def default_order
|
|
order(updated_at: :desc)
|
|
end
|
|
|
|
def search(params)
|
|
q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :name, :description, :post_ids)
|
|
q = q.text_attribute_matches(:description, params[:description_matches])
|
|
|
|
if params[:post_tags_match]
|
|
q = q.post_tags_match(params[:post_tags_match])
|
|
end
|
|
|
|
if params[:name_matches].present?
|
|
q = q.name_matches(params[:name_matches])
|
|
end
|
|
|
|
case params[:category]
|
|
when "series"
|
|
q = q.series
|
|
when "collection"
|
|
q = q.collection
|
|
end
|
|
|
|
case params[:order]
|
|
when "name"
|
|
q = q.order("pools.name")
|
|
when "created_at"
|
|
q = q.order("pools.created_at desc")
|
|
when "post_count"
|
|
q = q.order(Arel.sql("cardinality(post_ids) desc")).default_order
|
|
else
|
|
q = q.apply_default_order(params)
|
|
end
|
|
|
|
q
|
|
end
|
|
end
|
|
|
|
extend SearchMethods
|
|
|
|
def self.normalize_name(name)
|
|
name.gsub(/[_[:space:]]+/, "_").gsub(/\A_|_\z/, "")
|
|
end
|
|
|
|
def self.normalize_name_for_search(name)
|
|
normalize_name(name).mb_chars.downcase
|
|
end
|
|
|
|
def self.named(name)
|
|
if name =~ /^\d+$/
|
|
where(id: name.to_i)
|
|
elsif name
|
|
where_ilike(:name, normalize_name_for_search(name))
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def self.find_by_name(name)
|
|
named(name).try(:first)
|
|
end
|
|
|
|
def versions
|
|
raise NotImplementedError, "Archive service not configured" unless PoolVersion.enabled?
|
|
PoolVersion.where(pool_id: id).order("id asc")
|
|
end
|
|
|
|
def is_series?
|
|
category == "series"
|
|
end
|
|
|
|
def is_collection?
|
|
category == "collection"
|
|
end
|
|
|
|
def normalize_name
|
|
self.name = Pool.normalize_name(name)
|
|
end
|
|
|
|
def pretty_name
|
|
name.tr("_", " ")
|
|
end
|
|
|
|
def pretty_category
|
|
category.titleize
|
|
end
|
|
|
|
def normalize_post_ids
|
|
self.post_ids = post_ids.uniq if is_collection?
|
|
end
|
|
|
|
def revert_to!(version)
|
|
if id != version.pool_id
|
|
raise RevertError, "You cannot revert to a previous version of another pool."
|
|
end
|
|
|
|
self.post_ids = version.post_ids
|
|
self.name = version.name
|
|
self.description = version.description
|
|
save!
|
|
end
|
|
|
|
def contains?(post_id)
|
|
post_ids.include?(post_id)
|
|
end
|
|
|
|
def page_number(post_id)
|
|
post_ids.find_index(post_id).to_i + 1
|
|
end
|
|
|
|
def updater_can_edit_deleted
|
|
if is_deleted? && !Pundit.policy!(CurrentUser.user, self).update?
|
|
errors.add(:base, "You cannot update pools that are deleted")
|
|
end
|
|
end
|
|
|
|
def create_mod_action_for_delete
|
|
ModAction.log("deleted pool ##{id} (name: #{name})", :pool_delete)
|
|
end
|
|
|
|
def create_mod_action_for_undelete
|
|
ModAction.log("undeleted pool ##{id} (name: #{name})", :pool_undelete)
|
|
end
|
|
|
|
def add!(post)
|
|
return if contains?(post.id)
|
|
return if is_deleted?
|
|
|
|
with_lock do
|
|
update(post_ids: post_ids + [post.id])
|
|
end
|
|
end
|
|
|
|
def remove!(post)
|
|
return unless contains?(post.id)
|
|
|
|
with_lock do
|
|
reload
|
|
update(post_ids: post_ids - [post.id])
|
|
end
|
|
end
|
|
|
|
# XXX unify with PostQueryBuilder ordpool search
|
|
def posts
|
|
pool_posts = Pool.where(id: id).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 post_count
|
|
post_ids.size
|
|
end
|
|
|
|
def first_post?(post_id)
|
|
post_id == post_ids.first
|
|
end
|
|
|
|
def last_post?(post_id)
|
|
post_id == post_ids.last
|
|
end
|
|
|
|
# XXX finds wrong post when the pool contains multiple copies of the same post (#2042).
|
|
def previous_post_id(post_id)
|
|
return nil if first_post?(post_id) || !contains?(post_id)
|
|
|
|
n = post_ids.index(post_id) - 1
|
|
post_ids[n]
|
|
end
|
|
|
|
def next_post_id(post_id)
|
|
return nil if last_post?(post_id) || !contains?(post_id)
|
|
|
|
n = post_ids.index(post_id) + 1
|
|
post_ids[n]
|
|
end
|
|
|
|
def cover_post
|
|
post_count > 0 ? Post.find(post_ids.first) : nil
|
|
end
|
|
|
|
def create_version(updater: CurrentUser.user, updater_ip_addr: CurrentUser.ip_addr)
|
|
if PoolVersion.enabled?
|
|
PoolVersion.queue(self, updater, updater_ip_addr)
|
|
else
|
|
Rails.logger.warn("Archive service is not configured. Pool versions will not be saved.")
|
|
end
|
|
end
|
|
|
|
def last_page
|
|
(post_count / CurrentUser.user.per_page.to_f).ceil
|
|
end
|
|
|
|
def validate_name
|
|
case name
|
|
when /\A(any|none|series|collection)\z/i
|
|
errors.add(:name, "cannot be any of the following names: any, none, series, collection")
|
|
when /,/
|
|
errors.add(:name, "cannot contain commas")
|
|
when /\*/
|
|
errors.add(:name, "cannot contain asterisks")
|
|
when /\A_/
|
|
errors.add(:name, "cannot begin with an underscore")
|
|
when /_\z/
|
|
errors.add(:name, "cannot end with an underscore")
|
|
when /__/
|
|
errors.add(:name, "cannot contain consecutive underscores")
|
|
when /[^[:graph:]]/
|
|
errors.add(:name, "cannot contain non-printable characters")
|
|
when ""
|
|
errors.add(:name, "cannot be blank")
|
|
when /\A[0-9]+\z/
|
|
errors.add(:name, "cannot contain only digits")
|
|
end
|
|
end
|
|
end
|