Files
danbooru/app/models/pool.rb
evazion 7d503f088e posts: stop using pool_string attribute.
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.
2021-10-07 05:55:43 -05:00

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