Merge branch 'master' into attribute-searching

This commit is contained in:
evazion
2020-08-17 14:23:00 -05:00
committed by GitHub
155 changed files with 2834 additions and 2169 deletions

View File

@@ -127,6 +127,10 @@ class ApplicationRecord < ActiveRecord::Base
ensure
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env == "test"
end
def update!(*args)
all.each { |record| record.update!(*args) }
end
end
end

View File

@@ -6,11 +6,15 @@ class Ban < ApplicationRecord
after_destroy :create_unban_mod_action
belongs_to :user
belongs_to :banner, :class_name => "User"
validates_presence_of :reason, :duration
validate :user, :validate_user_is_bannable, on: :create
scope :unexpired, -> { where("bans.expires_at > ?", Time.now) }
scope :expired, -> { where("bans.expires_at <= ?", Time.now) }
attr_reader :duration
def self.is_banned?(user)
exists?(["user_id = ? AND expires_at > ?", user.id, Time.now])
end
@@ -48,6 +52,10 @@ class Ban < ApplicationRecord
end
end
def validate_user_is_bannable
self.errors[:user] << "is already banned" if user.is_banned?
end
def update_user_on_create
user.update!(is_banned: true)
end
@@ -69,8 +77,6 @@ class Ban < ApplicationRecord
@duration = dur
end
attr_reader :duration
def humanized_duration
ApplicationController.helpers.distance_of_time_in_words(created_at, expires_at)
end

View File

@@ -7,7 +7,8 @@ class Post < ApplicationRecord
class TimeoutError < StandardError; end
# Tags to copy when copying notes.
NOTE_COPY_TAGS = %w[translated partially_translated check_translation translation_request reverse_translation]
NOTE_COPY_TAGS = %w[translated partially_translated check_translation translation_request reverse_translation
annotated partially_annotated check_annotation annotation_request]
deletable
@@ -61,8 +62,10 @@ class Post < ApplicationRecord
scope :pending, -> { where(is_pending: true) }
scope :flagged, -> { where(is_flagged: true) }
scope :banned, -> { where(is_banned: true) }
scope :active, -> { where(is_pending: false, is_deleted: false, is_flagged: false) }
scope :pending_or_flagged, -> { pending.or(flagged) }
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) }
@@ -237,9 +240,9 @@ class Post < ApplicationRecord
def large_image_width
if has_large?
[Danbooru.config.large_image_width, image_width].min
[Danbooru.config.large_image_width, image_width.to_i].min
else
image_width
image_width.to_i
end
end
@@ -269,6 +272,7 @@ class Post < ApplicationRecord
end
def resize_percentage
return 100 if image_width.to_i == 0
100 * large_image_width.to_f / image_width.to_f
end
@@ -279,12 +283,28 @@ class Post < ApplicationRecord
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_pending? || is_flagged? || is_deleted?) && uploader != user
!is_status_locked? && !is_active? && uploader != user
end
def flag!(reason, is_deletion: false)
flag = flags.create(reason: reason, is_resolved: false, is_deletion: is_deletion, creator: CurrentUser.user)
flag = flags.create(reason: reason, is_deletion: is_deletion, creator: CurrentUser.user)
if flag.errors.any?
raise PostFlag::Error.new(flag.errors.full_messages.join("; "))
@@ -375,12 +395,6 @@ class Post < ApplicationRecord
def update_tag_post_counts
decrement_tags = tag_array_was - tag_array
decrement_tags_except_requests = decrement_tags.reject {|tag| tag == "tagme" || tag.end_with?("_request")}
if !decrement_tags_except_requests.empty? && !CurrentUser.is_builder? && CurrentUser.created_at > 1.week.ago
self.errors.add(:updater_id, "must have an account at least 1 week old to remove tags")
return false
end
increment_tags = tag_array - tag_array_was
if increment_tags.any?
Tag.increment_post_counts(increment_tags)
@@ -398,24 +412,16 @@ class Post < ApplicationRecord
set_tag_count(category, self.send("tag_count_#{category}") + 1)
end
def set_tag_counts(disable_cache = true)
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 => disable_cache)
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 fix_post_counts(post)
post.set_tag_counts(false)
if post.changes_saved?
args = Hash[TagCategory.categories.map {|x| ["tag_count_#{x}", post.send("tag_count_#{x}")]}].update(:tag_count => post.tag_count)
post.update_columns(args)
end
end
def merge_old_changes
reset_tag_array_cache
@removed_tags = []
@@ -932,14 +938,7 @@ class Post < ApplicationRecord
end
def update_children_on_destroy
return unless children.present?
eldest = children[0]
siblings = children[1..-1]
eldest.update(parent_id: nil)
Post.where(id: siblings).find_each { |p| p.update(parent_id: eldest.id) }
# Post.where(id: siblings).update(parent_id: eldest.id) # XXX rails 5
children.update(parent: nil)
end
def update_parent_on_save
@@ -949,7 +948,7 @@ class Post < ApplicationRecord
Post.find(parent_id_before_last_save).update_has_children_flag if parent_id_before_last_save.present?
end
def give_favorites_to_parent(options = {})
def give_favorites_to_parent
return if parent.nil?
transaction do
@@ -959,9 +958,7 @@ class Post < ApplicationRecord
end
end
unless options[:without_mod_action]
ModAction.log("moved favorites from post ##{id} to post ##{parent.id}", :post_move_favorites)
end
ModAction.log("moved favorites from post ##{id} to post ##{parent.id}", :post_move_favorites)
end
def has_visible_children?
@@ -985,9 +982,8 @@ class Post < ApplicationRecord
transaction do
Post.without_timeout do
ModAction.log("permanently deleted post ##{id}", :post_permanent_delete)
ModAction.log("permanently deleted post ##{id} (md5=#{md5})", :post_permanent_delete)
give_favorites_to_parent
update_children_on_destroy
decrement_tag_post_counts
remove_from_all_pools
@@ -1009,29 +1005,22 @@ class Post < ApplicationRecord
ModAction.log("unbanned post ##{id}", :post_unban)
end
def delete!(reason, options = {})
if is_status_locked?
self.errors.add(:is_status_locked, "; cannot delete post")
return false
end
def delete!(reason, move_favorites: false, user: CurrentUser.user)
transaction do
automated = (user == User.system)
Post.transaction do
flag!(reason, is_deletion: true)
flags.pending.update!(status: :succeeded)
appeals.pending.update!(status: :rejected)
update(
is_deleted: true,
is_pending: false,
is_flagged: false,
is_banned: is_banned || options[:ban] || has_tag?("banned_artist")
)
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(options) if options[:move_favorites]
give_favorites_to_parent if move_favorites
is_automatic = (reason == "Unapproved in three days")
uploader.upload_limit.update_limit!(self, incremental: is_automatic)
uploader.upload_limit.update_limit!(self, incremental: automated)
unless options[:without_mod_action]
unless automated
ModAction.log("deleted post ##{id}, reason: #{reason}", :post_delete)
end
end
@@ -1213,8 +1202,6 @@ class Post < ApplicationRecord
def with_flag_stats
relation = left_outer_joins(:flags).group(:id).select("posts.*")
relation = relation.select("COUNT(post_flags.id) AS flag_count")
relation = relation.select("COUNT(post_flags.id) FILTER (WHERE post_flags.is_resolved = TRUE) AS resolved_flag_count")
relation = relation.select("COUNT(post_flags.id) FILTER (WHERE post_flags.is_resolved = FALSE) AS unresolved_flag_count")
relation
end
@@ -1256,6 +1243,14 @@ class Post < ApplicationRecord
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?

View File

@@ -1,57 +1,38 @@
class PostAppeal < ApplicationRecord
class Error < StandardError; end
MAX_APPEALS_PER_DAY = 1
belongs_to :creator, :class_name => "User"
belongs_to :post
validates_presence_of :reason
validates :reason, presence: true, length: { in: 1..140 }
validate :validate_post_is_inactive
validate :validate_creator_is_not_limited
validates_uniqueness_of :creator_id, :scope => :post_id, :message => "have already appealed this post"
scope :resolved, -> { where(post: Post.undeleted.unflagged) }
scope :unresolved, -> { where(post: Post.deleted.or(Post.flagged)) }
scope :recent, -> { where("post_appeals.created_at >= ?", 1.day.ago) }
validates :reason, length: { maximum: 140 }
validate :validate_post_is_appealable, on: :create
validate :validate_creator_is_not_limited, on: :create
validates :creator, uniqueness: { scope: :post, message: "have already appealed this post" }, on: :create
enum status: {
pending: 0,
succeeded: 1,
rejected: 2
}
scope :expired, -> { pending.where("post_appeals.created_at < ?", Danbooru.config.moderation_period.ago) }
module SearchMethods
def search(params)
q = super
q = q.search_attributes(params, :reason)
q = q.search_attributes(params, :reason, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q = q.resolved if params[:is_resolved].to_s.truthy?
q = q.unresolved if params[:is_resolved].to_s.falsy?
q.apply_default_order(params)
end
end
extend SearchMethods
def resolved?
post.present? && !post.is_deleted? && !post.is_flagged?
end
def is_resolved
resolved?
end
def validate_creator_is_not_limited
if appeal_count_for_creator >= MAX_APPEALS_PER_DAY
errors[:creator] << "can appeal at most #{MAX_APPEALS_PER_DAY} post a day"
end
errors[:creator] << "have reached your appeal limit" if creator.is_appeal_limited?
end
def validate_post_is_inactive
if resolved?
errors[:post] << "is active"
end
end
def appeal_count_for_creator
creator.post_appeals.recent.count
def validate_post_is_appealable
errors[:post] << "cannot be appealed" if post.is_status_locked? || !post.is_appealable?
end
def self.searchable_includes

View File

@@ -12,7 +12,7 @@ class PostApproval < ApplicationRecord
errors.add(:post, "is locked and cannot be approved")
end
if post.status == "active"
if post.is_active?
errors.add(:post, "is already active and cannot be approved")
end
@@ -28,7 +28,9 @@ class PostApproval < ApplicationRecord
def approve_post
is_undeletion = post.is_deleted
post.flags.each(&:resolve!)
post.flags.pending.update!(status: :rejected)
post.appeals.pending.update!(status: :succeeded)
post.update(approver: user, is_flagged: false, is_pending: false, is_deleted: false)
ModAction.log("undeleted post ##{post_id}", :post_undelete) if is_undeletion

View File

@@ -50,7 +50,7 @@ class PostDisapproval < ApplicationRecord
end
def validate_disapproval
if post.status == "active"
if post.is_active?
errors[:post] << "is already active and cannot be disapproved"
end
end

View File

@@ -30,10 +30,6 @@ class PostEvent
event.try(:reason) || ""
end
def is_resolved
event.try(:is_resolved) || false
end
def creator_id
event.try(:creator_id) || event.try(:user_id)
end
@@ -42,6 +38,18 @@ class PostEvent
event.try(:creator) || event.try(:user)
end
def status
if event.is_a?(PostApproval)
"approved"
elsif (event.is_a?(PostAppeal) && event.succeeded?) || (event.is_a?(PostFlag) && event.rejected?)
"approved"
elsif (event.is_a?(PostAppeal) && event.rejected?) || (event.is_a?(PostFlag) && event.succeeded?)
"deleted"
else
"pending"
end
end
def is_creator_visible?(user = CurrentUser.user)
case event
when PostAppeal, PostApproval
@@ -57,7 +65,7 @@ class PostEvent
"creator_id": nil,
"created_at": nil,
"reason": nil,
"is_resolved": nil,
"status": nil,
"type": nil
}
end

View File

@@ -6,24 +6,26 @@ class PostFlag < ApplicationRecord
REJECTED = "Unapproved in three days after returning to moderation queue%"
end
COOLDOWN_PERIOD = 3.days
belongs_to :creator, class_name: "User"
belongs_to :post
validates :reason, presence: true, length: { in: 1..140 }
validate :validate_creator_is_not_limited, on: :create
validate :validate_post
validates_uniqueness_of :creator_id, :scope => :post_id, :on => :create, :unless => :is_deletion, :message => "have already flagged this post"
validate :validate_post, on: :create
validates_uniqueness_of :creator_id, scope: :post_id, on: :create, unless: :is_deletion, message: "have already flagged this post"
before_save :update_post
attr_accessor :is_deletion
enum status: {
pending: 0,
succeeded: 1,
rejected: 2
}
scope :by_users, -> { where.not(creator: User.system) }
scope :by_system, -> { where(creator: User.system) }
scope :in_cooldown, -> { by_users.where("created_at >= ?", COOLDOWN_PERIOD.ago) }
scope :resolved, -> { where(is_resolved: true) }
scope :unresolved, -> { where(is_resolved: false) }
scope :recent, -> { where("post_flags.created_at >= ?", 1.day.ago) }
scope :old, -> { where("post_flags.created_at <= ?", 3.days.ago) }
scope :in_cooldown, -> { by_users.where("created_at >= ?", Danbooru.config.moderation_period.ago) }
scope :expired, -> { pending.where("post_flags.created_at < ?", Danbooru.config.moderation_period.ago) }
scope :active, -> { pending.or(rejected.in_cooldown) }
module SearchMethods
def creator_matches(creator, searcher)
@@ -56,7 +58,7 @@ class PostFlag < ApplicationRecord
def search(params)
q = super
q = q.search_attributes(params, :is_resolved, :reason)
q = q.search_attributes(params, :reason, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:creator_id].present?
@@ -93,36 +95,18 @@ class PostFlag < ApplicationRecord
end
def validate_creator_is_not_limited
return if is_deletion
if creator.can_approve_posts?
# do nothing
elsif creator.created_at > 1.week.ago
errors[:creator] << "cannot flag within the first week of sign up"
elsif creator.is_gold? && flag_count_for_creator >= 10
errors[:creator] << "can flag 10 posts a day"
elsif !creator.is_gold? && flag_count_for_creator >= 1
errors[:creator] << "can flag 1 post a day"
end
flag = post.flags.in_cooldown.last
if flag.present?
errors[:post] << "cannot be flagged more than once every #{COOLDOWN_PERIOD.inspect} (last flagged: #{flag.created_at.to_s(:long)})"
end
errors[:creator] << "have reached your flag limit" if creator.is_flag_limited? && !is_deletion
end
def validate_post
errors[:post] << "is pending and cannot be flagged" if post.is_pending? && !is_deletion
errors[:post] << "is deleted and cannot be flagged" if post.is_deleted? && !is_deletion
errors[:post] << "is locked and cannot be flagged" if post.is_status_locked?
errors[:post] << "is deleted" if post.is_deleted?
end
def resolve!
update_column(:is_resolved, true)
end
def flag_count_for_creator
creator.post_flags.recent.count
flag = post.flags.in_cooldown.last
if !is_deletion && flag.present?
errors[:post] << "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_s(:long)})"
end
end
def uploader_id

View File

@@ -15,12 +15,6 @@ class PostVersion < ApplicationRecord
establish_connection database_url if enabled?
def self.check_for_retry(msg)
if msg =~ /can't get socket descriptor/ && msg =~ /post_versions/
connection.reconnect!
end
end
module SearchMethods
def changed_tags_include(tag)
where_array_includes_all(:added_tags, [tag]).or(where_array_includes_all(:removed_tags, [tag]))
@@ -32,6 +26,10 @@ class PostVersion < ApplicationRecord
end
end
def changed_tags_include_any(tags)
where_array_includes_any(:added_tags, tags).or(where_array_includes_any(:removed_tags, tags))
end
def tag_matches(string)
tag = string.match(/\S+/)[0]
return all if tag.nil?
@@ -47,6 +45,14 @@ class PostVersion < ApplicationRecord
q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/))
end
if params[:all_changed_tags]
q = q.changed_tags_include_all(params[:all_changed_tags].scan(/[^[:space:]]+/))
end
if params[:any_changed_tags]
q = q.changed_tags_include_any(params[:any_changed_tags].scan(/[^[:space:]]+/))
end
if params[:tag_matches]
q = q.tag_matches(params[:tag_matches])
end

View File

@@ -11,8 +11,8 @@ class Tag < ApplicationRecord
validates :name, tag_name: true, on: :name
validates_inclusion_of :category, in: TagCategory.category_ids
before_save :update_category_cache, if: :category_changed?
before_save :update_category_post_counts, if: :category_changed?
after_save :update_category_cache, if: :saved_change_to_category?
after_save :update_category_post_counts, if: :saved_change_to_category?
scope :empty, -> { where("tags.post_count <= 0") }
scope :nonempty, -> { where("tags.post_count > 0") }
@@ -163,12 +163,10 @@ class Tag < ApplicationRecord
end
def update_category_post_counts
Post.with_timeout(30_000, nil, :tags => name) do
Post.raw_tag_match(name).where("true /* Tag#update_category_post_counts */").find_each do |post|
post.reload
post.set_tag_counts(false)
args = TagCategory.categories.map {|x| ["tag_count_#{x}", post.send("tag_count_#{x}")]}.to_h.update(:tag_count => post.tag_count)
Post.where(:id => post.id).update_all(args)
Post.with_timeout(30_000) do
Post.raw_tag_match(name).find_each do |post|
post.set_tag_counts
post.save!
end
end
end

View File

@@ -265,6 +265,10 @@ class User < ApplicationRecord
name.match?(/\Auser_[0-9]+~*\z/)
end
def is_restricted?
requires_verification? && !is_verified?
end
def is_anonymous?
level == Levels::ANONYMOUS
end
@@ -343,6 +347,26 @@ class User < ApplicationRecord
end
end
def is_appeal_limited?
return false if can_upload_free?
upload_limit.free_upload_slots < UploadLimit::APPEAL_COST
end
def is_flag_limited?
return false if has_unlimited_flags?
post_flags.active.count >= 5
end
# Flags are unlimited if you're an approver or you have at least 30 flags
# in the last 3 months and have a 70% flag success rate.
def has_unlimited_flags?
return true if can_approve_posts?
recent_flags = post_flags.where("created_at >= ?", 3.months.ago)
flag_ratio = recent_flags.succeeded.count / recent_flags.count.to_f
recent_flags.count >= 30 && flag_ratio >= 0.70
end
def upload_limit
@upload_limit ||= UploadLimit.new(self)
end