All posts have non-null width and height now that unsupported filetypes have been purged. https://danbooru.donmai.us/forum_topics/18027
1523 lines
44 KiB
Ruby
1523 lines
44 KiB
Ruby
class Post < ApplicationRecord
|
|
class ApprovalError < StandardError; end
|
|
class DisapprovalError < StandardError; end
|
|
class RevertError < StandardError; end
|
|
class SearchError < StandardError; end
|
|
class DeletionError < StandardError; end
|
|
class TimeoutError < StandardError; end
|
|
|
|
# Tags to copy when copying notes.
|
|
NOTE_COPY_TAGS = %w[translated partially_translated check_translation translation_request reverse_translation
|
|
annotated partially_annotated check_annotation annotation_request]
|
|
|
|
deletable
|
|
|
|
before_validation :merge_old_changes
|
|
before_validation :normalize_tags
|
|
before_validation :strip_source
|
|
before_validation :parse_pixiv_id
|
|
before_validation :blank_out_nonexistent_parents
|
|
before_validation :remove_parent_loops
|
|
validates :md5, uniqueness: { message: ->(post, _data) { "duplicate: #{Post.find_by_md5(post.md5).id}" }}, on: :create
|
|
validates :rating, inclusion: { in: %w[s q e], message: "rating must be s, q, or e" }
|
|
validates :source, length: { maximum: 1200 }
|
|
validate :added_tags_are_valid
|
|
validate :removed_tags_are_valid
|
|
validate :has_artist_tag
|
|
validate :has_copyright_tag
|
|
validate :has_enough_tags
|
|
validate :post_is_not_its_own_parent
|
|
validate :updater_can_change_rating
|
|
validate :uploader_is_not_limited, on: :create
|
|
before_save :update_tag_post_counts
|
|
before_save :set_tag_counts
|
|
before_create :autoban
|
|
after_save :create_version
|
|
after_save :update_parent_on_save
|
|
after_save :apply_post_metatags
|
|
after_commit :delete_files, :on => :destroy
|
|
|
|
belongs_to :approver, class_name: "User", optional: true
|
|
belongs_to :uploader, :class_name => "User", :counter_cache => "post_upload_count"
|
|
belongs_to :parent, class_name: "Post", optional: true
|
|
has_one :upload, :dependent => :destroy
|
|
has_one :artist_commentary, :dependent => :destroy
|
|
has_one :pixiv_ugoira_frame_data, :class_name => "PixivUgoiraFrameData", :dependent => :destroy
|
|
has_many :flags, :class_name => "PostFlag", :dependent => :destroy
|
|
has_many :appeals, :class_name => "PostAppeal", :dependent => :destroy
|
|
has_many :votes, :class_name => "PostVote", :dependent => :destroy
|
|
has_many :notes, :dependent => :destroy
|
|
has_many :comments, :dependent => :destroy
|
|
has_many :children, -> {order("posts.id")}, :class_name => "Post", :foreign_key => "parent_id"
|
|
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
|
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
|
has_many :favorites
|
|
has_many :favorited_users, through: :favorites, source: :user
|
|
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
|
|
|
|
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning
|
|
|
|
scope :pending, -> { where(is_pending: true) }
|
|
scope :flagged, -> { where(is_flagged: true) }
|
|
scope :banned, -> { where(is_banned: true) }
|
|
# XXX conflict with deletable
|
|
scope :active, -> { where(is_pending: false, is_deleted: false, is_flagged: false).where.not(id: PostAppeal.pending) }
|
|
scope :appealed, -> { deleted.where(id: PostAppeal.pending.select(:post_id)) }
|
|
scope :in_modqueue, -> { pending.or(flagged).or(appealed) }
|
|
scope :expired, -> { pending.where("posts.created_at < ?", Danbooru.config.moderation_period.ago) }
|
|
|
|
scope :unflagged, -> { where(is_flagged: false) }
|
|
scope :has_notes, -> { where.not(last_noted_at: nil) }
|
|
scope :for_user, ->(user_id) { where(uploader_id: user_id) }
|
|
|
|
if PostVersion.enabled?
|
|
has_many :versions, -> { Rails.env.test? ? order("post_versions.updated_at ASC, post_versions.id ASC") : order("post_versions.updated_at ASC") }, class_name: "PostVersion", dependent: :destroy
|
|
end
|
|
|
|
module FileMethods
|
|
extend ActiveSupport::Concern
|
|
|
|
module ClassMethods
|
|
def delete_files(post_id, md5, file_ext, force: false)
|
|
if Post.exists?(md5: md5) && !force
|
|
raise DeletionError, "Files still in use; skipping deletion."
|
|
end
|
|
|
|
Danbooru.config.storage_manager.delete_file(post_id, md5, file_ext, :original)
|
|
Danbooru.config.storage_manager.delete_file(post_id, md5, file_ext, :large)
|
|
Danbooru.config.storage_manager.delete_file(post_id, md5, file_ext, :preview)
|
|
|
|
Danbooru.config.backup_storage_manager.delete_file(post_id, md5, file_ext, :original)
|
|
Danbooru.config.backup_storage_manager.delete_file(post_id, md5, file_ext, :large)
|
|
Danbooru.config.backup_storage_manager.delete_file(post_id, md5, file_ext, :preview)
|
|
end
|
|
end
|
|
|
|
def queue_delete_files(grace_period)
|
|
DeletePostFilesJob.set(wait: grace_period).perform_later(id, md5, file_ext)
|
|
end
|
|
|
|
def delete_files
|
|
Post.delete_files(id, md5, file_ext, force: true)
|
|
end
|
|
|
|
def distribute_files(file, sample_file, preview_file)
|
|
storage_manager.store_file(file, self, :original)
|
|
storage_manager.store_file(sample_file, self, :large) if sample_file.present?
|
|
storage_manager.store_file(preview_file, self, :preview) if preview_file.present?
|
|
|
|
backup_storage_manager.store_file(file, self, :original)
|
|
backup_storage_manager.store_file(sample_file, self, :large) if sample_file.present?
|
|
backup_storage_manager.store_file(preview_file, self, :preview) if preview_file.present?
|
|
end
|
|
|
|
def backup_storage_manager
|
|
Danbooru.config.backup_storage_manager
|
|
end
|
|
|
|
def storage_manager
|
|
Danbooru.config.storage_manager
|
|
end
|
|
|
|
def file(type = :original)
|
|
storage_manager.open_file(self, type)
|
|
end
|
|
|
|
def tagged_file_url
|
|
storage_manager.file_url(self, :original, tagged_filenames: !CurrentUser.user.disable_tagged_filenames?)
|
|
end
|
|
|
|
def tagged_large_file_url
|
|
storage_manager.file_url(self, :large, tagged_filenames: !CurrentUser.user.disable_tagged_filenames?)
|
|
end
|
|
|
|
def file_url
|
|
storage_manager.file_url(self, :original)
|
|
end
|
|
|
|
def large_file_url
|
|
storage_manager.file_url(self, :large)
|
|
end
|
|
|
|
def preview_file_url
|
|
storage_manager.file_url(self, :preview)
|
|
end
|
|
|
|
def file_path
|
|
storage_manager.file_path(self, file_ext, :original)
|
|
end
|
|
|
|
def large_file_path
|
|
storage_manager.file_path(self, file_ext, :large)
|
|
end
|
|
|
|
def preview_file_path
|
|
storage_manager.file_path(self, file_ext, :preview)
|
|
end
|
|
|
|
def crop_file_url
|
|
storage_manager.file_url(self, :crop)
|
|
end
|
|
|
|
def open_graph_image_url
|
|
if is_image?
|
|
if has_large?
|
|
large_file_url
|
|
else
|
|
file_url
|
|
end
|
|
else
|
|
preview_file_url
|
|
end
|
|
end
|
|
|
|
def file_url_for(user)
|
|
if user.default_image_size == "large" && image_width > Danbooru.config.large_image_width
|
|
tagged_large_file_url
|
|
else
|
|
tagged_file_url
|
|
end
|
|
end
|
|
|
|
def is_image?
|
|
file_ext =~ /jpg|gif|png/i
|
|
end
|
|
|
|
def is_png?
|
|
file_ext =~ /png/i
|
|
end
|
|
|
|
def is_gif?
|
|
file_ext =~ /gif/i
|
|
end
|
|
|
|
def is_flash?
|
|
file_ext =~ /swf/i
|
|
end
|
|
|
|
def is_webm?
|
|
file_ext =~ /webm/i
|
|
end
|
|
|
|
def is_mp4?
|
|
file_ext =~ /mp4/i
|
|
end
|
|
|
|
def is_video?
|
|
is_webm? || is_mp4?
|
|
end
|
|
|
|
def is_ugoira?
|
|
file_ext =~ /zip/i
|
|
end
|
|
|
|
def has_preview?
|
|
is_image? || is_video? || is_ugoira?
|
|
end
|
|
end
|
|
|
|
module ImageMethods
|
|
def twitter_card_supported?
|
|
image_width.to_i >= 280 && image_height.to_i >= 150
|
|
end
|
|
|
|
def has_large?
|
|
return false if has_tag?("animated_gif|animated_png")
|
|
return true if is_ugoira?
|
|
is_image? && image_width.present? && image_width > Danbooru.config.large_image_width
|
|
end
|
|
|
|
alias has_large has_large?
|
|
|
|
def large_image_width
|
|
if has_large?
|
|
[Danbooru.config.large_image_width, image_width.to_i].min
|
|
else
|
|
image_width.to_i
|
|
end
|
|
end
|
|
|
|
def large_image_height
|
|
ratio = Danbooru.config.large_image_width.to_f / image_width.to_f
|
|
if has_large? && ratio < 1
|
|
(image_height * ratio).to_i
|
|
else
|
|
image_height
|
|
end
|
|
end
|
|
|
|
def image_width_for(user)
|
|
if user.default_image_size == "large"
|
|
large_image_width
|
|
else
|
|
image_width
|
|
end
|
|
end
|
|
|
|
def image_height_for(user)
|
|
if user.default_image_size == "large"
|
|
large_image_height
|
|
else
|
|
image_height
|
|
end
|
|
end
|
|
|
|
def resize_percentage
|
|
return 100 if image_width.to_i == 0
|
|
100 * large_image_width.to_f / image_width.to_f
|
|
end
|
|
|
|
# XXX
|
|
def current_image_size
|
|
has_large? && CurrentUser.default_image_size == "large" ? "large" : "original"
|
|
end
|
|
end
|
|
|
|
module ApprovalMethods
|
|
def in_modqueue?
|
|
is_pending? || is_flagged? || is_appealed?
|
|
end
|
|
|
|
def is_active?
|
|
!is_deleted? && !in_modqueue?
|
|
end
|
|
|
|
def is_appealed?
|
|
is_deleted? && appeals.any?(&:pending?)
|
|
end
|
|
|
|
def is_appealable?
|
|
is_deleted? && !is_appealed?
|
|
end
|
|
|
|
def is_approvable?(user = CurrentUser.user)
|
|
!is_status_locked? && !is_active? && uploader != user
|
|
end
|
|
|
|
def flag!(reason, is_deletion: false)
|
|
flag = flags.create(reason: reason, is_deletion: is_deletion, creator: CurrentUser.user)
|
|
|
|
if flag.errors.any?
|
|
raise PostFlag::Error, flag.errors.full_messages.join("; ")
|
|
end
|
|
end
|
|
|
|
def approve!(approver = CurrentUser.user)
|
|
approvals.create(user: approver)
|
|
end
|
|
|
|
def disapproved_by?(user)
|
|
PostDisapproval.exists?(user_id: user.id, post_id: id)
|
|
end
|
|
|
|
def autoban
|
|
if has_tag?("banned_artist") || has_tag?("paid_reward")
|
|
self.is_banned = true
|
|
end
|
|
end
|
|
end
|
|
|
|
module PresenterMethods
|
|
def presenter
|
|
@presenter ||= PostPresenter.new(self)
|
|
end
|
|
|
|
def status_flags
|
|
flags = []
|
|
flags << "pending" if is_pending?
|
|
flags << "flagged" if is_flagged?
|
|
flags << "deleted" if is_deleted?
|
|
flags << "banned" if is_banned?
|
|
flags.join(" ")
|
|
end
|
|
|
|
def pretty_rating
|
|
case rating
|
|
when "q"
|
|
"Questionable"
|
|
|
|
when "e"
|
|
"Explicit"
|
|
|
|
when "s"
|
|
"Safe"
|
|
end
|
|
end
|
|
|
|
def normalized_source
|
|
Sources::Strategies.normalize_source(source)
|
|
end
|
|
|
|
def source_domain
|
|
return "" unless source =~ %r{\Ahttps?://}i
|
|
|
|
url = Addressable::URI.parse(normalized_source)
|
|
url.domain
|
|
rescue StandardError
|
|
""
|
|
end
|
|
end
|
|
|
|
module TagMethods
|
|
def tag_array
|
|
@tag_array ||= tag_string.split
|
|
end
|
|
|
|
def tag_array_was
|
|
@tag_array_was ||= (tag_string_in_database.presence || tag_string_before_last_save || "").split
|
|
end
|
|
|
|
def tags
|
|
Tag.where(name: tag_array)
|
|
end
|
|
|
|
def tags_was
|
|
Tag.where(name: tag_array_was)
|
|
end
|
|
|
|
def added_tags
|
|
tags - tags_was
|
|
end
|
|
|
|
def decrement_tag_post_counts
|
|
Tag.where(:name => tag_array).update_all("post_count = post_count - 1") if tag_array.any?
|
|
end
|
|
|
|
def update_tag_post_counts
|
|
decrement_tags = tag_array_was - tag_array
|
|
|
|
increment_tags = tag_array - tag_array_was
|
|
if increment_tags.any?
|
|
Tag.increment_post_counts(increment_tags)
|
|
end
|
|
if decrement_tags.any?
|
|
Tag.decrement_post_counts(decrement_tags)
|
|
end
|
|
end
|
|
|
|
def set_tag_count(category, tagcount)
|
|
send("tag_count_#{category}=", tagcount)
|
|
end
|
|
|
|
def inc_tag_count(category)
|
|
set_tag_count(category, send("tag_count_#{category}") + 1)
|
|
end
|
|
|
|
def set_tag_counts
|
|
self.tag_count = 0
|
|
TagCategory.categories.each {|x| set_tag_count(x, 0)}
|
|
categories = Tag.categories_for(tag_array, disable_caching: true)
|
|
categories.each_value do |category|
|
|
self.tag_count += 1
|
|
inc_tag_count(TagCategory.reverse_mapping[category])
|
|
end
|
|
end
|
|
|
|
def merge_old_changes
|
|
reset_tag_array_cache
|
|
@removed_tags = []
|
|
|
|
if old_tag_string
|
|
# If someone else committed changes to this post before we did,
|
|
# then try to merge the tag changes together.
|
|
current_tags = tag_string_was.split
|
|
new_tags = PostQueryBuilder.new(tag_string).parse_tag_edit
|
|
old_tags = old_tag_string.split
|
|
|
|
kept_tags = current_tags & new_tags
|
|
@removed_tags = old_tags - kept_tags
|
|
|
|
set_tag_string(((current_tags + new_tags) - old_tags + (current_tags & new_tags)).uniq.sort.join(" "))
|
|
end
|
|
|
|
if old_parent_id == ""
|
|
old_parent_id = nil
|
|
else
|
|
old_parent_id = old_parent_id.to_i
|
|
end
|
|
if old_parent_id == parent_id
|
|
self.parent_id = parent_id_before_last_save || parent_id_was
|
|
end
|
|
|
|
if old_source == source.to_s
|
|
self.source = source_before_last_save || source_was
|
|
end
|
|
|
|
if old_rating == rating
|
|
self.rating = rating_before_last_save || rating_was
|
|
end
|
|
end
|
|
|
|
def reset_tag_array_cache
|
|
@tag_array = nil
|
|
@tag_array_was = nil
|
|
end
|
|
|
|
def set_tag_string(string)
|
|
self.tag_string = string
|
|
reset_tag_array_cache
|
|
end
|
|
|
|
def normalize_tags
|
|
normalized_tags = PostQueryBuilder.new(tag_string).parse_tag_edit
|
|
normalized_tags = apply_casesensitive_metatags(normalized_tags)
|
|
normalized_tags = normalized_tags.map(&:downcase)
|
|
normalized_tags = filter_metatags(normalized_tags)
|
|
normalized_tags = TagAlias.to_aliased(normalized_tags)
|
|
normalized_tags = remove_negated_tags(normalized_tags)
|
|
normalized_tags = %w[tagme] if normalized_tags.empty?
|
|
normalized_tags = add_automatic_tags(normalized_tags)
|
|
normalized_tags = remove_invalid_tags(normalized_tags)
|
|
normalized_tags = Tag.convert_cosplay_tags(normalized_tags)
|
|
normalized_tags += Tag.create_for_list(Tag.automatic_tags_for(normalized_tags))
|
|
normalized_tags += TagImplication.tags_implied_by(normalized_tags).map(&:name)
|
|
normalized_tags = normalized_tags.compact.uniq.sort
|
|
normalized_tags = Tag.create_for_list(normalized_tags)
|
|
set_tag_string(normalized_tags.join(" "))
|
|
end
|
|
|
|
def remove_invalid_tags(tag_names)
|
|
invalid_tags = tag_names.map { |name| Tag.new(name: name) }.select { |tag| tag.invalid?(:name) }
|
|
|
|
invalid_tags.each do |tag|
|
|
tag.errors.messages.each do |attribute, messages|
|
|
warnings.add(:base, "Couldn't add tag: #{messages.join(';')}")
|
|
end
|
|
end
|
|
|
|
tag_names - invalid_tags.map(&:name)
|
|
end
|
|
|
|
def remove_negated_tags(tags)
|
|
@negated_tags, tags = tags.partition {|x| x =~ /\A-/i}
|
|
@negated_tags = @negated_tags.map {|x| x[1..-1]}
|
|
@negated_tags = TagAlias.to_aliased(@negated_tags)
|
|
tags - @negated_tags
|
|
end
|
|
|
|
def add_automatic_tags(tags)
|
|
tags -= %w[incredibly_absurdres absurdres highres lowres huge_filesize flash]
|
|
|
|
if image_width >= 10_000 || image_height >= 10_000
|
|
tags << "incredibly_absurdres"
|
|
end
|
|
if image_width >= 3200 || image_height >= 2400
|
|
tags << "absurdres"
|
|
end
|
|
if image_width >= 1600 || image_height >= 1200
|
|
tags << "highres"
|
|
end
|
|
if image_width <= 500 && image_height <= 500
|
|
tags << "lowres"
|
|
end
|
|
|
|
if image_width >= 1024 && image_width.to_f / image_height >= 4
|
|
tags << "wide_image"
|
|
tags << "long_image"
|
|
elsif image_height >= 1024 && image_height.to_f / image_width >= 4
|
|
tags << "tall_image"
|
|
tags << "long_image"
|
|
end
|
|
|
|
if file_size >= 10.megabytes
|
|
tags << "huge_filesize"
|
|
end
|
|
|
|
if is_flash?
|
|
tags << "flash"
|
|
end
|
|
|
|
if is_video?
|
|
tags << "video"
|
|
end
|
|
|
|
if is_ugoira?
|
|
tags << "ugoira"
|
|
end
|
|
|
|
if !is_gif?
|
|
tags -= ["animated_gif"]
|
|
end
|
|
|
|
if !is_png?
|
|
tags -= ["animated_png"]
|
|
end
|
|
|
|
tags
|
|
end
|
|
|
|
def apply_casesensitive_metatags(tags)
|
|
casesensitive_metatags, tags = tags.partition {|x| x =~ /\A(?:source):/i}
|
|
# Reuse the following metatags after the post has been saved
|
|
casesensitive_metatags += tags.select {|x| x =~ /\A(?:newpool):/i}
|
|
if !casesensitive_metatags.empty?
|
|
case casesensitive_metatags[-1]
|
|
when /^source:none$/i
|
|
self.source = ""
|
|
|
|
when /^source:"(.*)"$/i
|
|
self.source = $1
|
|
|
|
when /^source:(.*)$/i
|
|
self.source = $1
|
|
|
|
when /^newpool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
if pool.nil?
|
|
pool = Pool.create(name: $1, description: "This pool was automatically generated")
|
|
end
|
|
end
|
|
end
|
|
|
|
tags
|
|
end
|
|
|
|
def filter_metatags(tags)
|
|
@pre_metatags, tags = tags.partition {|x| x =~ /\A(?:rating|parent|-parent|-?locked):/i}
|
|
tags = apply_categorization_metatags(tags)
|
|
@post_metatags, tags = tags.partition {|x| x =~ /\A(?:-pool|pool|newpool|fav|-fav|child|-child|-favgroup|favgroup|upvote|downvote|status|-status|disapproved):/i}
|
|
apply_pre_metatags
|
|
tags
|
|
end
|
|
|
|
def apply_categorization_metatags(tags)
|
|
tags.map do |x|
|
|
if x =~ Tag.categories.regexp
|
|
tag = Tag.find_or_create_by_name(x)
|
|
tag.name
|
|
else
|
|
x
|
|
end
|
|
end
|
|
end
|
|
|
|
def apply_post_metatags
|
|
return unless @post_metatags
|
|
|
|
@post_metatags.each do |tag|
|
|
case tag
|
|
when /^-pool:(\d+)$/i
|
|
pool = Pool.find_by_id($1.to_i)
|
|
remove_pool!(pool) if pool
|
|
|
|
when /^-pool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
remove_pool!(pool) if pool
|
|
|
|
when /^pool:(\d+)$/i
|
|
pool = Pool.find_by_id($1.to_i)
|
|
add_pool!(pool) if pool
|
|
|
|
when /^pool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
add_pool!(pool) if pool
|
|
|
|
when /^newpool:(.+)$/i
|
|
pool = Pool.find_by_name($1)
|
|
add_pool!(pool) if pool
|
|
|
|
when /^fav:(.+)$/i
|
|
add_favorite(CurrentUser.user)
|
|
|
|
when /^-fav:(.+)$/i
|
|
remove_favorite(CurrentUser.user)
|
|
|
|
when /^(up|down)vote:(.+)$/i
|
|
score = ($1 == "up" ? 1 : -1)
|
|
vote!(score, CurrentUser.user)
|
|
|
|
when /^status:active$/i
|
|
raise User::PrivilegeError unless CurrentUser.is_approver?
|
|
approvals.create!(user: CurrentUser.user)
|
|
|
|
when /^status:banned$/i
|
|
raise User::PrivilegeError unless CurrentUser.is_approver?
|
|
ban!
|
|
|
|
when /^-status:banned$/i
|
|
raise User::PrivilegeError unless CurrentUser.is_approver?
|
|
unban!
|
|
|
|
when /^disapproved:(.+)$/i
|
|
raise User::PrivilegeError unless CurrentUser.is_approver?
|
|
disapprovals.create!(user: CurrentUser.user, reason: $1.downcase)
|
|
|
|
when /^child:none$/i
|
|
children.each do |post|
|
|
post.update!(parent_id: nil)
|
|
end
|
|
|
|
when /^-child:(.+)$/i
|
|
children.search(id: $1).each do |post|
|
|
post.update!(parent_id: nil)
|
|
end
|
|
|
|
when /^child:(.+)$/i
|
|
Post.search(id: $1).where.not(id: id).limit(10).each do |post|
|
|
post.update!(parent_id: id)
|
|
end
|
|
|
|
when /^-favgroup:(.+)$/i
|
|
favgroup = FavoriteGroup.find_by_name_or_id!($1, CurrentUser.user)
|
|
raise User::PrivilegeError unless Pundit.policy!(CurrentUser.user, favgroup).update?
|
|
favgroup&.remove!(self)
|
|
|
|
when /^favgroup:(.+)$/i
|
|
favgroup = FavoriteGroup.find_by_name_or_id!($1, CurrentUser.user)
|
|
raise User::PrivilegeError unless Pundit.policy!(CurrentUser.user, favgroup).update?
|
|
favgroup&.add!(self)
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
def apply_pre_metatags
|
|
return unless @pre_metatags
|
|
|
|
@pre_metatags.each do |tag|
|
|
case tag
|
|
when /^parent:none$/i, /^parent:0$/i
|
|
self.parent_id = nil
|
|
|
|
when /^-parent:(\d+)$/i
|
|
if parent_id == $1.to_i
|
|
self.parent_id = nil
|
|
end
|
|
|
|
when /^parent:(\d+)$/i
|
|
if $1.to_i != id && Post.exists?(["id = ?", $1.to_i])
|
|
self.parent_id = $1.to_i
|
|
remove_parent_loops
|
|
end
|
|
|
|
when /^rating:([qse])/i
|
|
self.rating = $1
|
|
|
|
when /^(-?)locked:notes?$/i
|
|
self.is_note_locked = ($1 != "-") if CurrentUser.is_builder?
|
|
|
|
when /^(-?)locked:rating$/i
|
|
self.is_rating_locked = ($1 != "-") if CurrentUser.is_builder?
|
|
|
|
when /^(-?)locked:status$/i
|
|
self.is_status_locked = ($1 != "-") if CurrentUser.is_admin?
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
def has_tag?(tag)
|
|
tag_string.match?(/(?:^| )(?:#{tag})(?:$| )/)
|
|
end
|
|
|
|
def add_tag(tag)
|
|
set_tag_string("#{tag_string} #{tag}")
|
|
end
|
|
|
|
def remove_tag(tag)
|
|
set_tag_string((tag_array - Array(tag)).join(" "))
|
|
end
|
|
|
|
def tag_categories
|
|
@tag_categories ||= Tag.categories_for(tag_array)
|
|
end
|
|
|
|
def typed_tags(name)
|
|
@typed_tags ||= {}
|
|
@typed_tags[name] ||= begin
|
|
tag_array.select do |tag|
|
|
tag_categories[tag] == TagCategory.mapping[name]
|
|
end
|
|
end
|
|
end
|
|
|
|
TagCategory.categories.each do |category|
|
|
define_method("tag_string_#{category}") do
|
|
typed_tags(category).join(" ")
|
|
end
|
|
end
|
|
end
|
|
|
|
module FavoriteMethods
|
|
def clean_fav_string?
|
|
true
|
|
end
|
|
|
|
def clean_fav_string!
|
|
array = fav_string.split.uniq
|
|
self.fav_string = array.join(" ")
|
|
self.fav_count = array.size
|
|
update_column(:fav_string, fav_string)
|
|
update_column(:fav_count, fav_count)
|
|
end
|
|
|
|
def favorited_by?(user)
|
|
return false if user.is_anonymous?
|
|
Favorite.exists?(post: self, user: user)
|
|
end
|
|
|
|
def append_user_to_fav_string(user_id)
|
|
update_column(:fav_string, (fav_string + " fav:#{user_id}").strip)
|
|
clean_fav_string! if clean_fav_string?
|
|
end
|
|
|
|
def add_favorite(user)
|
|
add_favorite!(user)
|
|
true
|
|
rescue Favorite::Error
|
|
false
|
|
end
|
|
|
|
def add_favorite!(user)
|
|
Favorite.add(post: self, user: user)
|
|
vote!(1, user)
|
|
end
|
|
|
|
def delete_user_from_fav_string(user_id)
|
|
update_column(:fav_string, fav_string.gsub(/(?:\A| )fav:#{user_id}(?:\Z| )/, " ").strip)
|
|
end
|
|
|
|
def remove_favorite!(user)
|
|
Favorite.remove(post: self, user: user)
|
|
unvote!(user)
|
|
end
|
|
|
|
def remove_favorite(user)
|
|
remove_favorite!(user)
|
|
true
|
|
rescue Favorite::Error
|
|
false
|
|
end
|
|
|
|
# Users who publicly favorited this post, ordered by time of favorite.
|
|
def visible_favorited_users(viewer)
|
|
favorited_users.order("favorites.id DESC").select do |fav_user|
|
|
Pundit.policy!(viewer, fav_user).can_see_favorites?
|
|
end
|
|
end
|
|
|
|
def favorite_groups
|
|
FavoriteGroup.for_post(id)
|
|
end
|
|
|
|
def remove_from_favorites
|
|
Favorite.where(post_id: id).delete_all
|
|
user_ids = fav_string.scan(/\d+/)
|
|
User.where(:id => user_ids).update_all("favorite_count = favorite_count - 1")
|
|
PostVote.where(post_id: id).delete_all
|
|
end
|
|
|
|
def remove_from_fav_groups
|
|
FavoriteGroup.for_post(id).find_each do |favgroup|
|
|
favgroup.remove!(self)
|
|
end
|
|
end
|
|
end
|
|
|
|
module PoolMethods
|
|
def pools
|
|
Pool.where("pools.post_ids && array[?]", id)
|
|
end
|
|
|
|
def has_active_pools?
|
|
pools.undeleted.present?
|
|
end
|
|
|
|
def belongs_to_pool?(pool)
|
|
pool_string =~ /(?:\A| )pool:#{pool.id}(?:\Z| )/
|
|
end
|
|
|
|
def belongs_to_pool_with_id?(pool_id)
|
|
pool_string =~ /(?:\A| )pool:#{pool_id}(?:\Z| )/
|
|
end
|
|
|
|
def add_pool!(pool, force = false)
|
|
return if belongs_to_pool?(pool)
|
|
return if pool.is_deleted? && !force
|
|
|
|
with_lock do
|
|
self.pool_string = "#{pool_string} pool:#{pool.id}".strip
|
|
update_column(:pool_string, pool_string) unless new_record?
|
|
pool.add!(self)
|
|
end
|
|
end
|
|
|
|
def remove_pool!(pool)
|
|
return unless belongs_to_pool?(pool)
|
|
|
|
with_lock do
|
|
self.pool_string = pool_string.gsub(/(?:\A| )pool:#{pool.id}(?:\Z| )/, " ").strip
|
|
update_column(:pool_string, pool_string) unless new_record?
|
|
pool.remove!(self)
|
|
end
|
|
end
|
|
|
|
def remove_from_all_pools
|
|
pools.find_each do |pool|
|
|
pool.remove!(self)
|
|
end
|
|
end
|
|
end
|
|
|
|
module VoteMethods
|
|
def vote!(score, voter)
|
|
# Ignore vote if user doesn't have permission to vote.
|
|
return unless Pundit.policy!(voter, PostVote).create?
|
|
|
|
with_lock do
|
|
votes.destroy_by(user: voter)
|
|
votes.create!(user: voter, score: score)
|
|
reload # PostVote.create modifies our score. Reload to get the new score.
|
|
end
|
|
end
|
|
|
|
def unvote!(voter)
|
|
return unless Pundit.policy!(voter, PostVote).create?
|
|
|
|
votes.destroy_by(user: voter)
|
|
reload
|
|
end
|
|
end
|
|
|
|
module ParentMethods
|
|
# A parent has many children. A child belongs to a parent.
|
|
# A parent cannot have a parent.
|
|
#
|
|
# After expunging a child:
|
|
# - Move favorites to parent.
|
|
# - Does the parent have any children?
|
|
# - Yes: Done.
|
|
# - No: Update parent's has_children flag to false.
|
|
#
|
|
# After expunging a parent:
|
|
# - Move favorites to the first child.
|
|
# - Reparent all children to the first child.
|
|
|
|
def update_has_children_flag
|
|
update(has_children: children.exists?, has_active_children: children.undeleted.exists?)
|
|
end
|
|
|
|
def blank_out_nonexistent_parents
|
|
if parent_id.present? && parent.nil?
|
|
self.parent_id = nil
|
|
end
|
|
end
|
|
|
|
def remove_parent_loops
|
|
if parent.present? && parent.parent_id.present? && parent.parent_id == id
|
|
parent.parent_id = nil
|
|
parent.save
|
|
end
|
|
end
|
|
|
|
def update_parent_on_destroy
|
|
parent&.update_has_children_flag
|
|
end
|
|
|
|
def update_children_on_destroy
|
|
children.update(parent: nil)
|
|
end
|
|
|
|
def update_parent_on_save
|
|
return unless saved_change_to_parent_id? || saved_change_to_is_deleted?
|
|
|
|
parent.update_has_children_flag if parent.present?
|
|
Post.find(parent_id_before_last_save).update_has_children_flag if parent_id_before_last_save.present?
|
|
end
|
|
|
|
def give_favorites_to_parent
|
|
return if parent.nil?
|
|
|
|
transaction do
|
|
favorites.each do |fav|
|
|
remove_favorite!(fav.user)
|
|
parent.add_favorite(fav.user)
|
|
end
|
|
end
|
|
|
|
ModAction.log("moved favorites from post ##{id} to post ##{parent.id}", :post_move_favorites)
|
|
end
|
|
|
|
def has_visible_children?
|
|
return true if has_active_children?
|
|
return true if has_children? && CurrentUser.user.show_deleted_children?
|
|
return true if has_children? && is_deleted?
|
|
return false
|
|
end
|
|
|
|
def has_visible_children
|
|
has_visible_children?
|
|
end
|
|
end
|
|
|
|
module DeletionMethods
|
|
def expunge!
|
|
if is_status_locked?
|
|
errors.add(:is_status_locked, "; cannot delete post")
|
|
return false
|
|
end
|
|
|
|
transaction do
|
|
Post.without_timeout do
|
|
ModAction.log("permanently deleted post ##{id} (md5=#{md5})", :post_permanent_delete)
|
|
|
|
update_children_on_destroy
|
|
decrement_tag_post_counts
|
|
remove_from_all_pools
|
|
remove_from_fav_groups
|
|
remove_from_favorites
|
|
destroy
|
|
update_parent_on_destroy
|
|
end
|
|
end
|
|
|
|
remove_iqdb # this is non-transactional
|
|
end
|
|
|
|
def ban!
|
|
update_column(:is_banned, true)
|
|
ModAction.log("banned post ##{id}", :post_ban)
|
|
end
|
|
|
|
def unban!
|
|
update_column(:is_banned, false)
|
|
ModAction.log("unbanned post ##{id}", :post_unban)
|
|
end
|
|
|
|
def delete!(reason, move_favorites: false, user: CurrentUser.user)
|
|
transaction do
|
|
automated = (user == User.system)
|
|
|
|
flags.pending.update!(status: :succeeded)
|
|
appeals.pending.update!(status: :rejected)
|
|
|
|
flags.create!(reason: reason, is_deletion: true, creator: user, status: :succeeded)
|
|
update!(is_deleted: true, is_pending: false, is_flagged: false)
|
|
|
|
# XXX This must happen *after* the `is_deleted` flag is set to true (issue #3419).
|
|
give_favorites_to_parent if move_favorites
|
|
|
|
uploader.upload_limit.update_limit!(is_pending?, false)
|
|
|
|
unless automated
|
|
ModAction.log("deleted post ##{id}, reason: #{reason}", :post_delete)
|
|
end
|
|
end
|
|
end
|
|
|
|
def replace!(params)
|
|
transaction do
|
|
replacement = replacements.create(params)
|
|
processor = UploadService::Replacer.new(post: self, replacement: replacement)
|
|
processor.process!
|
|
replacement
|
|
end
|
|
end
|
|
end
|
|
|
|
module VersionMethods
|
|
def create_version(force = false)
|
|
if new_record? || saved_change_to_watched_attributes? || force
|
|
create_new_version
|
|
end
|
|
end
|
|
|
|
def saved_change_to_watched_attributes?
|
|
saved_change_to_rating? || saved_change_to_source? || saved_change_to_parent_id? || saved_change_to_tag_string?
|
|
end
|
|
|
|
def merge_version?
|
|
prev = versions.last
|
|
prev && prev.updater_id == CurrentUser.user.id && prev.updated_at > 1.hour.ago
|
|
end
|
|
|
|
def create_new_version
|
|
User.where(id: CurrentUser.id).update_all("post_update_count = post_update_count + 1")
|
|
PostVersion.queue(self) if PostVersion.enabled?
|
|
end
|
|
|
|
def revert_to(target)
|
|
if id != target.post_id
|
|
raise RevertError, "You cannot revert to a previous version of another post."
|
|
end
|
|
|
|
self.tag_string = target.tags
|
|
self.rating = target.rating
|
|
self.source = target.source
|
|
self.parent_id = target.parent_id
|
|
end
|
|
|
|
def revert_to!(target)
|
|
revert_to(target)
|
|
save!
|
|
end
|
|
end
|
|
|
|
module NoteMethods
|
|
def has_notes?
|
|
last_noted_at.present?
|
|
end
|
|
|
|
def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS)
|
|
transaction do
|
|
if id == other_post.id
|
|
errors.add(:base, "Source and destination posts are the same")
|
|
return false
|
|
end
|
|
unless has_notes?
|
|
errors.add(:post, "has no notes")
|
|
return false
|
|
end
|
|
|
|
notes.active.each do |note|
|
|
note.copy_to(other_post)
|
|
end
|
|
|
|
dummy = Note.new
|
|
if notes.active.length == 1
|
|
dummy.body = "Copied 1 note from post ##{id}."
|
|
else
|
|
dummy.body = "Copied #{notes.active.length} notes from post ##{id}."
|
|
end
|
|
dummy.is_active = false
|
|
dummy.post_id = other_post.id
|
|
dummy.x = dummy.y = dummy.width = dummy.height = 0
|
|
dummy.save
|
|
|
|
copy_tags.each do |tag|
|
|
other_post.remove_tag(tag)
|
|
other_post.add_tag(tag) if has_tag?(tag)
|
|
end
|
|
|
|
other_post.has_embedded_notes = has_embedded_notes
|
|
other_post.save
|
|
end
|
|
end
|
|
end
|
|
|
|
module ApiMethods
|
|
def legacy_attributes
|
|
hash = {
|
|
"has_comments" => last_commented_at.present?,
|
|
"parent_id" => parent_id,
|
|
"status" => status,
|
|
"has_children" => has_children?,
|
|
"created_at" => created_at.to_formatted_s(:db),
|
|
"has_notes" => has_notes?,
|
|
"rating" => rating,
|
|
"author" => uploader.name,
|
|
"creator_id" => uploader_id,
|
|
"width" => image_width,
|
|
"source" => source,
|
|
"score" => score,
|
|
"tags" => tag_string,
|
|
"height" => image_height,
|
|
"file_size" => file_size,
|
|
"id" => id
|
|
}
|
|
|
|
if visible?
|
|
hash["file_url"] = file_url
|
|
hash["preview_url"] = preview_file_url
|
|
hash["md5"] = md5
|
|
end
|
|
|
|
hash
|
|
end
|
|
|
|
def status
|
|
if is_pending?
|
|
"pending"
|
|
elsif is_deleted?
|
|
"deleted"
|
|
elsif is_flagged?
|
|
"flagged"
|
|
else
|
|
"active"
|
|
end
|
|
end
|
|
end
|
|
|
|
module SearchMethods
|
|
# Return a set of up to N random posts. May return less if there aren't
|
|
# enough posts.
|
|
#
|
|
# @param n [Integer] The maximum number of posts to return
|
|
# @return [ActiveRecord::Relation<Post>]
|
|
def random(n = 1)
|
|
posts = n.times.map do
|
|
key = SecureRandom.hex(16)
|
|
random_up(key) || random_down(key)
|
|
end.compact.uniq
|
|
|
|
reorder(nil).find_ordered(posts.map(&:id))
|
|
end
|
|
|
|
def random_up(key)
|
|
where("md5 < ?", key).reorder("md5 desc").first
|
|
end
|
|
|
|
def random_down(key)
|
|
where("md5 >= ?", key).reorder("md5 asc").first
|
|
end
|
|
|
|
def sample(query, sample_size)
|
|
user_tag_match(query, safe_mode: false, hide_deleted_posts: false).reorder(:md5).limit(sample_size)
|
|
end
|
|
|
|
# unflattens the tag_string into one tag per row.
|
|
def with_unflattened_tags
|
|
joins("CROSS JOIN unnest(string_to_array(tag_string, ' ')) AS tag")
|
|
end
|
|
|
|
def with_comment_stats
|
|
relation = left_outer_joins(:comments).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(comments.id) AS comment_count")
|
|
relation = relation.select("COUNT(comments.id) FILTER (WHERE comments.is_deleted = TRUE) AS deleted_comment_count")
|
|
relation = relation.select("COUNT(comments.id) FILTER (WHERE comments.is_deleted = FALSE) AS active_comment_count")
|
|
relation
|
|
end
|
|
|
|
def with_note_stats
|
|
relation = left_outer_joins(:notes).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(notes.id) AS note_count")
|
|
relation = relation.select("COUNT(notes.id) FILTER (WHERE notes.is_active = TRUE) AS active_note_count")
|
|
relation = relation.select("COUNT(notes.id) FILTER (WHERE notes.is_active = FALSE) AS deleted_note_count")
|
|
relation
|
|
end
|
|
|
|
def with_flag_stats
|
|
relation = left_outer_joins(:flags).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(post_flags.id) AS flag_count")
|
|
relation
|
|
end
|
|
|
|
def with_appeal_stats
|
|
relation = left_outer_joins(:appeals).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(post_appeals.id) AS appeal_count")
|
|
relation
|
|
end
|
|
|
|
def with_approval_stats
|
|
relation = left_outer_joins(:approvals).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(post_approvals.id) AS approval_count")
|
|
relation
|
|
end
|
|
|
|
def with_replacement_stats
|
|
relation = left_outer_joins(:replacements).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(post_replacements.id) AS replacement_count")
|
|
relation
|
|
end
|
|
|
|
def with_child_stats
|
|
relation = left_outer_joins(:children).group(:id).select("posts.*")
|
|
relation = relation.select("COUNT(children_posts.id) AS child_count")
|
|
relation = relation.select("COUNT(children_posts.id) FILTER (WHERE children_posts.is_deleted = TRUE) AS deleted_child_count")
|
|
relation = relation.select("COUNT(children_posts.id) FILTER (WHERE children_posts.is_deleted = FALSE) AS active_child_count")
|
|
relation
|
|
end
|
|
|
|
def with_pool_stats
|
|
pool_posts = Pool.joins("CROSS JOIN unnest(post_ids) AS post_id").select(:id, :is_deleted, :category, "post_id")
|
|
relation = joins("LEFT OUTER JOIN (#{pool_posts.to_sql}) pools ON pools.post_id = posts.id").group(:id).select("posts.*")
|
|
|
|
relation = relation.select("COUNT(pools.id) AS pool_count")
|
|
relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.is_deleted = TRUE) AS deleted_pool_count")
|
|
relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.is_deleted = FALSE) AS active_pool_count")
|
|
relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.category = 'series') AS series_pool_count")
|
|
relation = relation.select("COUNT(pools.id) FILTER (WHERE pools.category = 'collection') AS collection_pool_count")
|
|
relation
|
|
end
|
|
|
|
def with_queued_at
|
|
relation = group(:id)
|
|
relation = relation.left_outer_joins(:flags, :appeals)
|
|
relation = relation.select("posts.*")
|
|
relation = relation.select(Arel.sql("MAX(GREATEST(posts.created_at, post_flags.created_at, post_appeals.created_at)) AS queued_at"))
|
|
relation
|
|
end
|
|
|
|
def with_stats(tables)
|
|
return all if tables.empty?
|
|
|
|
relation = all
|
|
tables.each do |table|
|
|
relation = relation.send("with_#{table}_stats")
|
|
end
|
|
|
|
from(relation.arel.as("posts"))
|
|
end
|
|
|
|
def available_for_moderation(user, hidden: false)
|
|
return none if user.is_anonymous?
|
|
|
|
approved_posts = user.post_approvals.select(:post_id)
|
|
disapproved_posts = user.post_disapprovals.select(:post_id)
|
|
|
|
if hidden.present?
|
|
where("posts.uploader_id = ? OR posts.id IN (#{approved_posts.to_sql}) OR posts.id IN (#{disapproved_posts.to_sql})", user.id)
|
|
else
|
|
where.not(uploader: user).where.not(id: approved_posts).where.not(id: disapproved_posts)
|
|
end
|
|
end
|
|
|
|
def raw_tag_match(tag)
|
|
where("posts.tag_index @@ to_tsquery('danbooru', E?)", tag.to_escaped_for_tsquery)
|
|
end
|
|
|
|
# Perform a tag search as an anonymous user. No tag limit is enforced.
|
|
def anon_tag_match(query)
|
|
user_tag_match(query, User.anonymous, tag_limit: nil, safe_mode: false, hide_deleted_posts: false)
|
|
end
|
|
|
|
# Perform a tag search as the system user, DanbooruBot. The search will
|
|
# have moderator-level permissions. No tag limit is enforced.
|
|
def system_tag_match(query)
|
|
user_tag_match(query, User.system, tag_limit: nil, safe_mode: false, hide_deleted_posts: false)
|
|
end
|
|
|
|
# Perform a tag search as the current user, or as another user.
|
|
#
|
|
# @param query [String] the tag search to perform
|
|
# @param user [User] the user to perform the search as
|
|
# @param tag_limit [Integer] the maximum number of tags allowed per search.
|
|
# An exception will be raised if the search has too many tags.
|
|
# @param safe_mode [Boolean] if true, automatically add rating:s to the search
|
|
# @param hide_deleted_posts [Boolean] if true, automatically add -status:deleted to the search
|
|
# @return [ActiveRecord::Relation<Post>] the set of resulting posts
|
|
def user_tag_match(query, user = CurrentUser.user, tag_limit: user.tag_query_limit, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?)
|
|
post_query = PostQueryBuilder.new(query, user, tag_limit: tag_limit, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts)
|
|
post_query.normalized_query.build
|
|
end
|
|
|
|
def search(params)
|
|
q = search_attributes(
|
|
params,
|
|
:id, :created_at, :updated_at, :rating, :source, :pixiv_id, :fav_count,
|
|
:score, :up_score, :down_score, :md5, :file_ext, :file_size, :image_width,
|
|
:image_height, :tag_count, :has_children, :has_active_children,
|
|
:is_note_locked, :is_rating_locked, :is_status_locked, :is_pending,
|
|
:is_flagged, :is_deleted, :is_banned, :last_comment_bumped_at,
|
|
:last_commented_at, :last_noted_at, :uploader_ip_addr,
|
|
:uploader, :approver, :parent, :upload, :artist_commentary,
|
|
:flags, :appeals, :notes, :comments, :children, :approvals,
|
|
:replacements, :pixiv_ugoira_frame_data
|
|
)
|
|
|
|
if params[:tags].present?
|
|
q = q.user_tag_match(params[:tags])
|
|
end
|
|
|
|
if params[:order].present?
|
|
q = PostQueryBuilder.new(nil).search_order(q, params[:order])
|
|
else
|
|
q = q.apply_default_order(params)
|
|
end
|
|
|
|
q
|
|
end
|
|
end
|
|
|
|
module PixivMethods
|
|
def parse_pixiv_id
|
|
self.pixiv_id = nil
|
|
|
|
site = Sources::Strategies::Pixiv.new(source)
|
|
if site.match?
|
|
self.pixiv_id = site.illust_id
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :RegenerationMethods do
|
|
def regenerate_later!(category, user)
|
|
RegeneratePostJob.perform_later(post: self, category: category, user: user)
|
|
end
|
|
|
|
def regenerate!(category, user)
|
|
if category == "iqdb"
|
|
update_iqdb
|
|
|
|
ModAction.log("<@#{user.name}> regenerated IQDB for post ##{id}", :post_regenerate_iqdb, user)
|
|
else
|
|
media_file = MediaFile.open(file, frame_data: pixiv_ugoira_frame_data&.data.to_a)
|
|
UploadService::Utils.process_resizes(self, nil, id, media_file: media_file)
|
|
|
|
purge_cached_urls!
|
|
update_iqdb
|
|
|
|
ModAction.log("<@#{user.name}> regenerated image samples for post ##{id}", :post_regenerate, user)
|
|
end
|
|
end
|
|
|
|
def purge_cached_urls!
|
|
urls = [preview_file_url, large_file_url]
|
|
CloudflareService.new.purge_cache(urls)
|
|
end
|
|
end
|
|
|
|
concerning :IqdbMethods do
|
|
def update_iqdb
|
|
# performs IqdbClient.new.add_post(post)
|
|
IqdbAddPostJob.perform_later(self)
|
|
end
|
|
|
|
def remove_iqdb
|
|
# performs IqdbClient.new.remove(id)
|
|
IqdbRemovePostJob.perform_later(id)
|
|
end
|
|
end
|
|
|
|
module ValidationMethods
|
|
def post_is_not_its_own_parent
|
|
if !new_record? && id == parent_id
|
|
errors.add(:base, "Post cannot have itself as a parent")
|
|
end
|
|
end
|
|
|
|
def updater_can_change_rating
|
|
# Don't forbid changes if the rating lock was just now set in the same update.
|
|
if rating_changed? && is_rating_locked? && !is_rating_locked_changed?
|
|
errors.add(:rating, "is locked and cannot be changed. Unlock the post first.")
|
|
end
|
|
end
|
|
|
|
def uploader_is_not_limited
|
|
errors.add(:uploader, "have reached your upload limit") if uploader.upload_limit.limited?
|
|
end
|
|
|
|
def added_tags_are_valid
|
|
new_tags = added_tags.select(&:empty?)
|
|
new_artist_tags, new_general_tags = new_tags.partition(&:artist?)
|
|
|
|
if new_general_tags.present?
|
|
n = new_general_tags.size
|
|
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
|
|
warnings.add(:base, "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}")
|
|
end
|
|
|
|
new_artist_tags.each do |tag|
|
|
if tag.artist.blank?
|
|
new_artist_path = Routes.new_artist_path(artist: { name: tag.name })
|
|
warnings.add(:base, "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[#{new_artist_path}]")
|
|
end
|
|
end
|
|
end
|
|
|
|
def removed_tags_are_valid
|
|
attempted_removed_tags = @removed_tags + @negated_tags
|
|
unremoved_tags = tag_array & attempted_removed_tags
|
|
|
|
if unremoved_tags.present?
|
|
unremoved_tags_list = unremoved_tags.map { |t| "[[#{t}]]" }.to_sentence
|
|
warnings.add(:base, "#{unremoved_tags_list} could not be removed. Check for implications and try again")
|
|
end
|
|
end
|
|
|
|
def has_artist_tag
|
|
return if !new_record?
|
|
return if source !~ %r{\Ahttps?://}
|
|
return if has_tag?("artist_request") || has_tag?("official_art")
|
|
return if tags.any?(&:artist?)
|
|
return if Sources::Strategies.find(source).is_a?(Sources::Strategies::Null)
|
|
|
|
new_artist_path = Routes.new_artist_path(artist: { source: source })
|
|
warnings.add(:base, "Artist tag is required. \"Create new artist tag\":[#{new_artist_path}]. Ask on the forum if you need naming help")
|
|
end
|
|
|
|
def has_copyright_tag
|
|
return if !new_record?
|
|
return if has_tag?("copyright_request") || tags.any?(&:copyright?)
|
|
|
|
warnings.add(:base, "Copyright tag is required. Consider adding [[copyright request]] or [[original]]")
|
|
end
|
|
|
|
def has_enough_tags
|
|
return if !new_record?
|
|
|
|
if tags.count(&:general?) < 10
|
|
warnings.add(:base, "Uploads must have at least 10 general tags. Read [[howto:tag]] for guidelines on tagging your uploads")
|
|
end
|
|
end
|
|
end
|
|
|
|
include FileMethods
|
|
include ImageMethods
|
|
include ApprovalMethods
|
|
include PresenterMethods
|
|
include TagMethods
|
|
include FavoriteMethods
|
|
include PoolMethods
|
|
include VoteMethods
|
|
include ParentMethods
|
|
include DeletionMethods
|
|
include VersionMethods
|
|
include NoteMethods
|
|
include ApiMethods
|
|
extend SearchMethods
|
|
include PixivMethods
|
|
include ValidationMethods
|
|
|
|
has_bit_flags ["has_embedded_notes", "has_cropped"]
|
|
|
|
def safeblocked?
|
|
CurrentUser.safe_mode? && (rating != "s" || Danbooru.config.safe_mode_restricted_tags.any? { |tag| tag.in?(tag_array) })
|
|
end
|
|
|
|
def levelblocked?(user = CurrentUser.user)
|
|
!user.is_gold? && Danbooru.config.restricted_tags.any? { |tag| tag.in?(tag_array) }
|
|
end
|
|
|
|
def banblocked?(user = CurrentUser.user)
|
|
return false unless is_banned?
|
|
(has_tag?("paid_reward") && !user.is_approver?) || !user.is_gold?
|
|
end
|
|
|
|
def visible?(user = CurrentUser.user)
|
|
!safeblocked? && !levelblocked?(user) && !banblocked?(user)
|
|
end
|
|
|
|
def reload(options = nil)
|
|
super
|
|
reset_tag_array_cache
|
|
@pools = nil
|
|
@tag_categories = nil
|
|
@typed_tags = nil
|
|
self
|
|
end
|
|
|
|
def strip_source
|
|
self.source = source.try(:strip)
|
|
end
|
|
|
|
def mark_as_translated(params)
|
|
add_tag("check_translation") if params["check_translation"].to_s.truthy?
|
|
remove_tag("check_translation") if params["check_translation"].to_s.falsy?
|
|
|
|
add_tag("partially_translated") if params["partially_translated"].to_s.truthy?
|
|
remove_tag("partially_translated") if params["partially_translated"].to_s.falsy?
|
|
|
|
if has_tag?("check_translation") || has_tag?("partially_translated")
|
|
add_tag("translation_request")
|
|
remove_tag("translated")
|
|
else
|
|
add_tag("translated")
|
|
remove_tag("translation_request")
|
|
end
|
|
|
|
save
|
|
end
|
|
|
|
def self.model_restriction(table)
|
|
super.where(table[:is_pending].eq(false)).where(table[:is_flagged].eq(false)).where(table[:is_deleted].eq(false))
|
|
end
|
|
|
|
def self.available_includes
|
|
# attributes accessible through the ?only= parameter
|
|
[:uploader, :updater, :approver, :upload, :flags, :appeals,
|
|
:parent, :children, :notes, :comments, :approvals, :disapprovals,
|
|
:replacements, :pixiv_ugoira_frame_data, :artist_commentary]
|
|
end
|
|
end
|