Fix #4568: Send appealed posts back to the mod queue
* Include appealed posts in the modqueue. * Add `status` field to appeals. Appeals start out as `pending`, then become `rejected` if the post isn't approved within three days. If the post is approved, the appeal's status becomes `succeeded`. * Add `status` field to flags. Flags start out as `pending` then become `rejected` if the post is approved within three days. If the post isn't approved, the flag's status becomes `succeeded`. * Leave behind a "Unapproved in three days" dummy flag when an appeal goes unapproved, just like when a pending post is unapproved. * Only allow deleted posts to be appealed. Don't allow flagged posts to be appealed. * Add `status:appealed` metatag. `status:appealed` is separate from `status:pending`. * Include appealed posts in `status:modqueue`. Search `status:modqueue order:modqueue` to view the modqueue as a normal search. * Retroactively set old flags and appeals as succeeded or rejected. This may not be correct for posts that were appealed or flagged multiple times. This is difficult to set correctly because we don't have approval records for old posts, so we can't tell the actual outcome of old flags and appeals. * Deprecate the `is_resolved` field on post flags. A resolved flag is a flag that isn't pending. * Known bug: appealed posts have a black border instead of a blue border. Checking whether a post has been appealed would require either an extra query on the posts/index page, or an is_appealed flag on posts, neither of which are very desirable. * Known bug: you can't use `status:appealed` in blacklists, for the same reason as above.
This commit is contained in:
@@ -4,14 +4,14 @@ class ModqueueController < ApplicationController
|
||||
|
||||
def index
|
||||
authorize :modqueue
|
||||
@posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).pending_or_flagged.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden])
|
||||
@posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).in_modqueue.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden])
|
||||
@posts = @posts.paginated_search(params, order: "modqueue", count_pages: true)
|
||||
|
||||
@modqueue_posts = @posts.except(:offset, :limit, :order)
|
||||
@pending_post_count = @modqueue_posts.pending.count
|
||||
@flagged_post_count = @modqueue_posts.flagged.count
|
||||
@disapproval_reasons = PostDisapproval.where(post: @modqueue_posts).where.not(reason: "disinterest").group(:reason).order(count: :desc).distinct.count(:post_id)
|
||||
@uploaders = @modqueue_posts.group(:uploader).order(count: :desc).limit(20).count
|
||||
@modqueue_posts = @posts.reselect(nil).reorder(nil).offset(nil).limit(nil)
|
||||
@pending_post_count = @modqueue_posts.select(&:is_pending?).count
|
||||
@flagged_post_count = @modqueue_posts.select(&:is_flagged?).count
|
||||
@disapproval_reasons = PostDisapproval.where(post: @modqueue_posts.reselect(:id)).where.not(reason: "disinterest").group(:reason).order(count: :desc).distinct.count(:post_id)
|
||||
@uploaders = @modqueue_posts.map(&:uploader).tally.sort_by(&:last).reverse.take(20).to_h
|
||||
|
||||
@tags = RelatedTagCalculator.frequent_tags_for_post_relation(@modqueue_posts)
|
||||
@artist_tags = @tags.select(&:artist?).sort_by(&:overlap_count).reverse.take(10)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
class PostPruner
|
||||
module PostPruner
|
||||
module_function
|
||||
|
||||
def prune!
|
||||
prune_pending!
|
||||
prune_flagged!
|
||||
prune_appealed!
|
||||
end
|
||||
|
||||
def prune_pending!
|
||||
@@ -11,10 +14,14 @@ class PostPruner
|
||||
end
|
||||
|
||||
def prune_flagged!
|
||||
Post.flagged.each do |post|
|
||||
if post.flags.unresolved.old.any?
|
||||
post.delete!("Unapproved in three days after returning to moderation queue", user: User.system)
|
||||
end
|
||||
PostFlag.expired.each do |flag|
|
||||
flag.post.delete!("Unapproved in three days after returning to moderation queue", user: User.system)
|
||||
end
|
||||
end
|
||||
|
||||
def prune_appealed!
|
||||
PostAppeal.expired.each do |appeal|
|
||||
appeal.post.delete!("Unapproved in three days after returning to moderation queue", user: User.system)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -274,8 +274,10 @@ class PostQueryBuilder
|
||||
Post.pending
|
||||
when "flagged"
|
||||
Post.flagged
|
||||
when "appealed"
|
||||
Post.appealed
|
||||
when "modqueue"
|
||||
Post.pending_or_flagged
|
||||
Post.in_modqueue
|
||||
when "deleted"
|
||||
Post.deleted
|
||||
when "banned"
|
||||
@@ -283,7 +285,7 @@ class PostQueryBuilder
|
||||
when "active"
|
||||
Post.active
|
||||
when "unmoderated"
|
||||
Post.pending_or_flagged.available_for_moderation(current_user, hidden: false)
|
||||
Post.in_modqueue.available_for_moderation(current_user, hidden: false)
|
||||
when "all", "any"
|
||||
Post.all
|
||||
else
|
||||
@@ -307,7 +309,7 @@ class PostQueryBuilder
|
||||
Post.where(parent: nil)
|
||||
when "any"
|
||||
Post.where.not(parent: nil)
|
||||
when /pending|flagged|modqueue|deleted|banned|active|unmoderated/
|
||||
when "pending", "flagged", "appealed", "modqueue", "deleted", "banned", "active", "unmoderated"
|
||||
Post.where.not(parent: nil).where(parent: status_matches(parent))
|
||||
when /\A\d+\z/
|
||||
Post.where(id: parent).or(Post.where(parent: parent))
|
||||
@@ -322,7 +324,7 @@ class PostQueryBuilder
|
||||
Post.where(has_children: false)
|
||||
when "any"
|
||||
Post.where(has_children: true)
|
||||
when /pending|flagged|modqueue|deleted|banned|active|unmoderated/
|
||||
when "pending", "flagged", "appealed", "modqueue", "deleted", "banned", "active", "unmoderated"
|
||||
Post.where(has_children: true).where(children: status_matches(child))
|
||||
else
|
||||
Post.none
|
||||
@@ -606,10 +608,10 @@ class PostQueryBuilder
|
||||
.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"))
|
||||
relation = relation.with_queued_at.order("queued_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"))
|
||||
relation = relation.with_queued_at.order("queued_at ASC, posts.id ASC")
|
||||
|
||||
when "none"
|
||||
relation = relation.reorder(nil)
|
||||
|
||||
@@ -149,7 +149,7 @@ module PostSets
|
||||
end
|
||||
|
||||
def not_shown(post)
|
||||
post.is_deleted? && tag_string !~ /status:(?:all|any|deleted|banned)/
|
||||
post.is_deleted? && tag_string !~ /status:(?:all|any|deleted|banned|modqueue)/
|
||||
end
|
||||
|
||||
def none_shown
|
||||
|
||||
@@ -111,6 +111,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
|
||||
|
||||
|
||||
@@ -61,8 +61,9 @@ 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, -> { where(id: PostAppeal.pending.select(:post_id)) }
|
||||
scope :in_modqueue, -> { pending.or(flagged).or(appealed) }
|
||||
scope :expired, -> { where("posts.created_at < ?", 3.days.ago) }
|
||||
|
||||
scope :unflagged, -> { where(is_flagged: false) }
|
||||
@@ -281,8 +282,24 @@ 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)
|
||||
@@ -991,7 +1008,10 @@ class Post < ApplicationRecord
|
||||
transaction do
|
||||
automated = (user == User.system)
|
||||
|
||||
flags.create!(reason: reason, is_deletion: true, creator: user)
|
||||
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).
|
||||
@@ -1224,6 +1244,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?
|
||||
|
||||
|
||||
@@ -5,20 +5,27 @@ class PostAppeal < ApplicationRecord
|
||||
|
||||
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"
|
||||
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 :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) }
|
||||
scope :expired, -> { pending.where("post_appeals.created_at <= ?", 3.days.ago) }
|
||||
|
||||
module SearchMethods
|
||||
def search(params)
|
||||
q = super
|
||||
q = q.search_attributes(params, :creator, :post, :reason)
|
||||
q = q.search_attributes(params, :creator, :post, :reason, :status)
|
||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||
|
||||
q = q.resolved if params[:is_resolved].to_s.truthy?
|
||||
@@ -44,10 +51,8 @@ class PostAppeal < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def validate_post_is_inactive
|
||||
if resolved?
|
||||
errors[:post] << "is active"
|
||||
end
|
||||
def validate_post_is_appealable
|
||||
errors[:post] << "cannot be appealed" if post.is_status_locked? || !post.is_appealable?
|
||||
end
|
||||
|
||||
def appeal_count_for_creator
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -46,7 +46,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
|
||||
|
||||
@@ -17,13 +17,19 @@ class PostFlag < ApplicationRecord
|
||||
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 :expired, -> { pending.where("post_flags.created_at <= ?", 3.days.ago) }
|
||||
|
||||
module SearchMethods
|
||||
def creator_matches(creator, searcher)
|
||||
@@ -56,7 +62,7 @@ class PostFlag < ApplicationRecord
|
||||
def search(params)
|
||||
q = super
|
||||
|
||||
q = q.search_attributes(params, :post, :is_resolved, :reason)
|
||||
q = q.search_attributes(params, :post, :is_resolved, :reason, :status)
|
||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||
|
||||
if params[:creator_id].present?
|
||||
@@ -113,12 +119,8 @@ class PostFlag < ApplicationRecord
|
||||
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ class PostPresenter
|
||||
return "<em>none</em>".html_safe
|
||||
end
|
||||
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /status:(?:all|any|deleted|banned)/
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /status:(?:all|any|deleted|banned|modqueue)/
|
||||
return ""
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%= content_tag(:div, { id: "post-#{post.id}", class: ["post", "mod-queue-preview", "column-container", *PostPresenter.preview_class(post)].join(" ") }.merge(PostPresenter.data_attributes(post))) do %>
|
||||
<aside class="column column-shrink">
|
||||
<%= PostPresenter.preview(post, size: true) %>
|
||||
<%= PostPresenter.preview(post, size: true, show_deleted: true) %>
|
||||
</aside>
|
||||
|
||||
<section class="column column-expand">
|
||||
|
||||
@@ -43,22 +43,18 @@
|
||||
Status:
|
||||
<% if post.is_pending? %>
|
||||
Pending
|
||||
<% end %>
|
||||
|
||||
<% if post.is_deleted? %>
|
||||
Deleted
|
||||
<% end %>
|
||||
|
||||
<% if post.is_flagged? %>
|
||||
<% elsif post.is_flagged? %>
|
||||
Flagged
|
||||
<% elsif post.is_appealed? %>
|
||||
Appealed
|
||||
<% elsif post.is_deleted? %>
|
||||
Deleted
|
||||
<% else %>
|
||||
Active
|
||||
<% end %>
|
||||
|
||||
<% if post.is_banned? %>
|
||||
Banned
|
||||
<% end %>
|
||||
|
||||
<% if !post.is_pending? && !post.is_deleted? && !post.is_banned? %>
|
||||
Active
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% if post.is_flagged? && !post.is_deleted? && post.flags.any? %>
|
||||
<% if post.is_flagged? %>
|
||||
<div class="notice notice-small post-notice post-notice-flagged">
|
||||
<p>This post was flagged for review (<%= link_to_wiki "learn more", "howto:flag" %>): </p>
|
||||
|
||||
@@ -30,13 +30,14 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if post.is_pending? || post.is_flagged? %>
|
||||
<% if post.in_modqueue? %>
|
||||
<div class="notice notice-small post-notice post-notice-pending">
|
||||
<% if post.is_pending? %>
|
||||
This post is pending approval.
|
||||
(<%= link_to_wiki "learn more", "about:mod_queue" %>)
|
||||
<% else %>
|
||||
This post is pending approval. (<%= link_to_wiki "learn more", "about:mod_queue" %>)
|
||||
<% elsif post.is_flagged? %>
|
||||
This post was flagged and is pending approval (<%= link_to_wiki "learn more", "about:mod_queue" %>)
|
||||
<% elsif post.is_appealed? %>
|
||||
This post was appealed and is pending approval (<%= link_to_wiki "learn more", "about:mod_queue" %>)
|
||||
<% end %>
|
||||
|
||||
<%= render "post_disapprovals/counts", :disapprovals => post.disapprovals, :post => post %>
|
||||
@@ -48,7 +49,8 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if (post.is_flagged? || post.is_deleted?) && post.appeals.any? %>
|
||||
<% #XXX %>
|
||||
<% if post.is_deleted? && post.appeals.any? %>
|
||||
<div class="notice notice-small post-notice post-notice-appealed">
|
||||
<p>This post was appealed:</p>
|
||||
<%= render "post_appeals/reasons", appeals: post.appeals %>
|
||||
|
||||
@@ -51,25 +51,24 @@
|
||||
<% if post.is_status_locked? %>
|
||||
<li id="post-option-status-locked">Status locked</li>
|
||||
<% else %>
|
||||
<% if (!post.is_deleted? && !post.is_pending? && !post.is_flagged?) && policy(PostFlag).create? %>
|
||||
<% if post.is_active? && policy(PostFlag).create? %>
|
||||
<li id="post-option-flag"><%= link_to "Flag", new_post_flag_path(post_flag: { post_id: post.id }), remote: true %></li>
|
||||
<% elsif (post.is_flagged? || post.is_deleted?) && policy(PostAppeal).create? %>
|
||||
<% elsif post.is_appealable? && policy(PostAppeal).create? %>
|
||||
<li id="post-option-appeal"><%= link_to "Appeal", new_post_appeal_path(post_appeal: { post_id: post.id }), remote: true %></li>
|
||||
<% end %>
|
||||
|
||||
<% if policy(PostApproval).create? %>
|
||||
<% if post.is_deleted? %>
|
||||
<li id="post-option-undelete"><%= link_to "Undelete", post_approvals_path(post_id: post.id), remote: true, method: :post, "data-confirm": "Are you sure you want to undelete this post?" %></li>
|
||||
<% if policy(post).move_favorites? %>
|
||||
<li id="post-option-move-favorites"><%= link_to "Move favorites", confirm_move_favorites_moderator_post_post_path(post_id: post.id) %></li>
|
||||
<% end %>
|
||||
<% elsif policy(post).delete? %>
|
||||
<li id="post-option-delete"><%= link_to "Delete", post, method: :delete, remote: true %></li>
|
||||
<% if post.is_approvable? %>
|
||||
<li id="post-option-approve"><%= link_to (post.is_deleted? ? "Undelete" : "Approve"), post_approvals_path(post_id: post.id), remote: true, method: :post, "data-shortcut": "shift+o", "data-confirm": "Are you sure you want to approve this post?" %></li>
|
||||
<li id="post-option-disapprove"><%= link_to "Hide from queue", post_disapprovals_path(post_disapproval: { post_id: post.id, reason: "disinterest" }), remote: true, method: :post %></li>
|
||||
<% end %>
|
||||
|
||||
<% if post.is_approvable? && !post.is_deleted? %>
|
||||
<li id="post-option-approve"><%= link_to "Approve", post_approvals_path(post_id: post.id), remote: true, method: :post, id: "approve", "data-shortcut": "shift+o", "data-confirm": "Are you sure you want to approve this post?" %></li>
|
||||
<li id="post-option-disapprove"><%= link_to "Hide from queue", post_disapprovals_path(post_disapproval: { post_id: post.id, reason: "disinterest" }), remote: true, method: :post, id: "disapprove" %></li>
|
||||
<% if post.is_deleted? && policy(post).move_favorites? %>
|
||||
<li id="post-option-move-favorites"><%= link_to "Move favorites", confirm_move_favorites_moderator_post_post_path(post_id: post.id) %></li>
|
||||
<% end %>
|
||||
|
||||
<% if !post.is_deleted? && policy(post).delete? %>
|
||||
<li id="post-option-delete"><%= link_to "Delete", post, method: :delete, remote: true %></li>
|
||||
<% end %>
|
||||
|
||||
<% if policy(post).unban? %>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class AddStatusToPostAppealsAndFlags < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :post_appeals, :status, :integer, default: 0, null: false
|
||||
add_index :post_appeals, :status
|
||||
|
||||
add_column :post_flags, :status, :integer, default: 0, null: false
|
||||
add_index :post_flags, :status
|
||||
end
|
||||
end
|
||||
@@ -2584,7 +2584,8 @@ CREATE TABLE public.post_appeals (
|
||||
creator_id integer NOT NULL,
|
||||
reason text,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
status integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -2684,7 +2685,8 @@ CREATE TABLE public.post_flags (
|
||||
reason text,
|
||||
is_resolved boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
status integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -6622,6 +6624,13 @@ CREATE INDEX index_post_appeals_on_post_id ON public.post_appeals USING btree (p
|
||||
CREATE INDEX index_post_appeals_on_reason_tsvector ON public.post_appeals USING gin (to_tsvector('english'::regconfig, reason));
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_appeals_on_status; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_post_appeals_on_status ON public.post_appeals USING btree (status);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_approvals_on_post_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -6671,6 +6680,13 @@ CREATE INDEX index_post_flags_on_post_id ON public.post_flags USING btree (post_
|
||||
CREATE INDEX index_post_flags_on_reason_tsvector ON public.post_flags USING gin (to_tsvector('english'::regconfig, reason));
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_flags_on_status; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_post_flags_on_status ON public.post_flags USING btree (status);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_post_replacements_on_creator_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -7393,6 +7409,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20200325074859'),
|
||||
('20200403210353'),
|
||||
('20200406054838'),
|
||||
('20200427190519');
|
||||
('20200427190519'),
|
||||
('20200803022359');
|
||||
|
||||
|
||||
|
||||
26
script/fixes/066_initialize_appeal_and_flag_statuses.rb
Executable file
26
script/fixes/066_initialize_appeal_and_flag_statuses.rb
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../../config/environment"
|
||||
|
||||
PostFlag.transaction do
|
||||
# Mark all old flags and appeals as succeeded or rejected. Recent flags and
|
||||
# appeals are left as pending. This is not strictly correct for posts that
|
||||
# may have been flagged or appealed multiple times.
|
||||
PostAppeal.expired.where(post: Post.undeleted).update_all(status: "succeeded")
|
||||
PostAppeal.expired.where(post: Post.deleted).update_all(status: "rejected")
|
||||
PostFlag.where(post: Post.undeleted).update_all(status: "rejected")
|
||||
PostFlag.where(post: Post.deleted).update_all(status: "succeeded")
|
||||
|
||||
# Mark all unapproved in three days flags as successful.
|
||||
PostFlag.category_matches("deleted").update_all(status: "succeeded")
|
||||
|
||||
# Mark all currently flagged posts as pending.
|
||||
PostFlag.where(post: Post.flagged).update_all(status: "pending")
|
||||
|
||||
puts "Appeals pending: #{PostAppeal.pending.count}"
|
||||
puts "Appeals succeeded: #{PostAppeal.succeeded.count}"
|
||||
puts "Appeals rejected: #{PostAppeal.rejected.count}"
|
||||
puts "Flags pending: #{PostFlag.pending.count}"
|
||||
puts "Flags succeeded: #{PostFlag.succeeded.count}"
|
||||
puts "Flags rejected: #{PostFlag.rejected.count}"
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
FactoryBot.define do
|
||||
factory(:post_flag) do
|
||||
creator
|
||||
post
|
||||
post { build(:post, is_flagged: true) }
|
||||
reason {"xxx"}
|
||||
is_resolved {false}
|
||||
end
|
||||
|
||||
@@ -13,6 +13,14 @@ class ModqueueControllerTest < ActionDispatch::IntegrationTest
|
||||
get_auth modqueue_index_path, @admin
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
should "include appealed posts in the modqueue" do
|
||||
@appeal = create(:post_appeal)
|
||||
get_auth modqueue_index_path, @admin
|
||||
|
||||
assert_response :success
|
||||
assert_select "#post-#{@appeal.post_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,14 +48,54 @@ class PostAppealsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
context "create action" do
|
||||
setup do
|
||||
@post = as(@user) { create(:post, is_deleted: true) }
|
||||
context "appealing a deleted post" do
|
||||
should "create a new appeal" do
|
||||
@post = create(:post, is_deleted: true)
|
||||
|
||||
assert_difference("PostAppeal.count", 1) do
|
||||
post_auth post_appeals_path, @user, params: { post_appeal: { post_id: @post.id, reason: "xxx" }}, as: :json
|
||||
end
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
|
||||
should "create a new appeal" do
|
||||
assert_difference("PostAppeal.count", 1) do
|
||||
post_auth post_appeals_path, @user, params: {:format => "js", :post_appeal => {:post_id => @post.id, :reason => "xxx"}}
|
||||
assert_response :success
|
||||
context "appealing a flagged post" do
|
||||
should "fail" do
|
||||
@flag = create(:post_flag)
|
||||
|
||||
assert_no_difference("PostAppeal.count") do
|
||||
post_auth post_appeals_path, @user, params: { post_appeal: { post_id: @flag.post.id, reason: "xxx" }}, as: :json
|
||||
end
|
||||
|
||||
assert_response 422
|
||||
assert_equal(["cannot be appealed"], response.parsed_body.dig("errors", "post"))
|
||||
end
|
||||
end
|
||||
|
||||
context "appealing a pending post" do
|
||||
should "fail" do
|
||||
@post = create(:post, is_pending: true)
|
||||
|
||||
assert_no_difference("PostAppeal.count") do
|
||||
post_auth post_appeals_path, @user, params: { post_appeal: { post_id: @post.id, reason: "xxx" }}, as: :json
|
||||
end
|
||||
|
||||
assert_response 422
|
||||
assert_equal(["cannot be appealed"], response.parsed_body.dig("errors", "post"))
|
||||
end
|
||||
end
|
||||
|
||||
context "appealing an already appealed post" do
|
||||
should "fail" do
|
||||
@appeal = create(:post_appeal)
|
||||
|
||||
assert_no_difference("PostAppeal.count") do
|
||||
post_auth post_appeals_path, @user, params: { post_appeal: { post_id: @appeal.post.id, reason: "xxx" }}, as: :json
|
||||
end
|
||||
|
||||
assert_response 422
|
||||
assert_equal(["cannot be appealed"], response.parsed_body.dig("errors", "post"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,6 +27,29 @@ class PostApprovalsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
context "for an appealed post" do
|
||||
should "undelete the post and mark the appeal as successful" do
|
||||
@appeal = create(:post_appeal)
|
||||
post_auth post_approvals_path(post_id: @appeal.post_id, format: :js), @approver
|
||||
|
||||
assert_response :success
|
||||
assert_equal(false, @appeal.reload.post.is_deleted?)
|
||||
assert_equal(true, @appeal.succeeded?)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a flagged post" do
|
||||
should "approve the post and mark the flag as rejected" do
|
||||
@flag = create(:post_flag)
|
||||
post_auth post_approvals_path(post_id: @flag.post_id, format: :js), @approver
|
||||
|
||||
assert_response :success
|
||||
assert_equal(false, @flag.reload.post.is_deleted?)
|
||||
assert_equal(false, @flag.post.is_flagged?)
|
||||
assert_equal(true, @flag.rejected?)
|
||||
end
|
||||
end
|
||||
|
||||
should "not allow non-approvers to approve posts" do
|
||||
@post = create(:post, is_pending: true)
|
||||
post_auth post_approvals_path(post_id: @post.id, format: :js), create(:user)
|
||||
|
||||
@@ -10,6 +10,7 @@ class PostEventsControllerTest < ActionDispatch::IntegrationTest
|
||||
as(@user) do
|
||||
@post = create(:post)
|
||||
@post.flag!("aaa")
|
||||
@post.update(is_deleted: true)
|
||||
create(:post_appeal, post: @post)
|
||||
@post.approve!(@mod)
|
||||
end
|
||||
|
||||
@@ -483,7 +483,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
|
||||
create(:note, post: @post)
|
||||
create(:artist_commentary, post: @post)
|
||||
create(:post_flag, post: @post, creator: @user)
|
||||
create(:post_appeal, post: @post, creator: @user)
|
||||
#create(:post_appeal, post: @post, creator: @user)
|
||||
create(:post_vote, post: @post, user: @user)
|
||||
create(:favorite, post: @post, user: @user)
|
||||
create(:moderation_report, model: @comment, creator: @builder)
|
||||
@@ -667,7 +667,6 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
assert_equal(false, @post.is_deleted?)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "revert action" do
|
||||
|
||||
@@ -32,7 +32,7 @@ class PostAppealTest < ActiveSupport::TestCase
|
||||
@post_appeal = build(:post_appeal, post: @post, creator: @alice)
|
||||
|
||||
assert_equal(false, @post_appeal.valid?)
|
||||
assert_equal(["Post is active"], @post_appeal.errors.full_messages)
|
||||
assert_equal(["Post cannot be appealed"], @post_appeal.errors.full_messages)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ class PostEventTest < ActiveSupport::TestCase
|
||||
@user = create(:user, created_at: 2.weeks.ago)
|
||||
@post = create(:post)
|
||||
@post_flag = create(:post_flag, creator: @user, post: @post)
|
||||
@post.update(is_deleted: true)
|
||||
@post_appeal = create(:post_appeal, creator: @user, post: @post)
|
||||
end
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class PostFlagTest < ActiveSupport::TestCase
|
||||
|
||||
@post_flag = build(:post_flag, post: @post, creator: @bob)
|
||||
@post_flag.save
|
||||
assert_equal(["Post is deleted"], @post_flag.errors.full_messages)
|
||||
assert_equal(["Post is deleted and cannot be flagged"], @post_flag.errors.full_messages)
|
||||
end
|
||||
|
||||
should "not be able to flag a pending post" do
|
||||
|
||||
@@ -1,30 +1,108 @@
|
||||
require 'test_helper'
|
||||
|
||||
class PostPrunerTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@old_post = FactoryBot.create(:post, :created_at => 5.days.ago, :is_pending => true)
|
||||
@unresolved_flagged_post = FactoryBot.create(:post, :is_flagged => true)
|
||||
@resolved_flagged_post = FactoryBot.create(:post, :is_flagged => true)
|
||||
context "PostPruner" do
|
||||
context "for a pending post" do
|
||||
should "prune expired posts" do
|
||||
@post = create(:post, created_at: 5.days.ago, is_pending: true)
|
||||
PostPruner.prune!
|
||||
|
||||
@flagger = create(:gold_user, created_at: 2.weeks.ago)
|
||||
@unresolved_post_flag = create(:post_flag, creator: @flagger, created_at: 5.days.ago, is_resolved: false, post: @unresolved_flagged_post)
|
||||
@resolved_post_flag = create(:post_flag, creator: @flagger, created_at: 5.days.ago, is_resolved: true, post: @resolved_flagged_post)
|
||||
assert_equal(true, @post.reload.is_deleted?)
|
||||
assert_equal(false, @post.is_pending?)
|
||||
|
||||
PostPruner.new.prune!
|
||||
end
|
||||
assert_equal(1, @post.flags.size)
|
||||
assert_equal("Unapproved in three days", @post.flags.last.reason)
|
||||
end
|
||||
end
|
||||
|
||||
should "prune old pending posts" do
|
||||
@old_post.reload
|
||||
assert(@old_post.is_deleted?)
|
||||
end
|
||||
context "for a flagged post" do
|
||||
should "prune expired flags" do
|
||||
@post = create(:post, created_at: 4.weeks.ago, is_flagged: true)
|
||||
@flag = create(:post_flag, post: @post, created_at: 5.days.ago)
|
||||
PostPruner.prune!
|
||||
|
||||
should "prune old flagged posts that are still unresolved" do
|
||||
@unresolved_flagged_post.reload
|
||||
assert(@unresolved_flagged_post.is_deleted?)
|
||||
end
|
||||
assert_equal(true, @post.reload.is_deleted?)
|
||||
assert_equal(false, @post.is_pending?)
|
||||
assert_equal(false, @post.is_flagged?)
|
||||
assert_equal(true, @flag.reload.succeeded?)
|
||||
|
||||
should "not prune old flagged posts that are resolved" do
|
||||
@resolved_flagged_post.reload
|
||||
assert(!@resolved_flagged_post.is_deleted?)
|
||||
assert_equal(2, @post.flags.size)
|
||||
assert_equal("Unapproved in three days after returning to moderation queue", @post.flags.last.reason)
|
||||
end
|
||||
|
||||
should "not prune unexpired flags" do
|
||||
@post = create(:post, created_at: 4.weeks.ago, is_flagged: true)
|
||||
@flag = create(:post_flag, post: @post, created_at: 1.day.ago)
|
||||
PostPruner.prune!
|
||||
|
||||
assert_equal(false, @post.reload.is_deleted?)
|
||||
assert_equal(false, @post.is_pending?)
|
||||
assert_equal(true, @post.is_flagged?)
|
||||
assert_equal(true, @flag.reload.pending?)
|
||||
|
||||
assert_equal(1, @post.flags.size)
|
||||
end
|
||||
|
||||
should "leave the status of old flags unchanged" do
|
||||
@post = create(:post, created_at: 4.weeks.ago, is_flagged: true)
|
||||
@flag1 = create(:post_flag, post: @post, created_at: 3.weeks.ago, status: :succeeded)
|
||||
@flag2 = create(:post_flag, post: @post, created_at: 2.weeks.ago, status: :rejected)
|
||||
@flag3 = create(:post_flag, post: @post, created_at: 1.weeks.ago, status: :pending)
|
||||
PostPruner.prune!
|
||||
|
||||
assert_equal(true, @post.reload.is_deleted?)
|
||||
assert_equal(false, @post.is_pending?)
|
||||
assert_equal(false, @post.is_flagged?)
|
||||
|
||||
assert_equal(true, @flag1.reload.succeeded?)
|
||||
assert_equal(true, @flag2.reload.rejected?)
|
||||
assert_equal(true, @flag3.reload.succeeded?)
|
||||
end
|
||||
end
|
||||
|
||||
context "for an appealed post" do
|
||||
should "prune expired appeals" do
|
||||
@post = create(:post, created_at: 4.weeks.ago, is_deleted: true)
|
||||
@appeal = create(:post_appeal, post: @post, created_at: 5.days.ago)
|
||||
PostPruner.prune!
|
||||
|
||||
assert_equal(false, @post.reload.is_pending?)
|
||||
assert_equal(false, @post.is_flagged?)
|
||||
assert_equal(true, @post.is_deleted?)
|
||||
assert_equal(true, @appeal.reload.rejected?)
|
||||
|
||||
assert_equal(1, @post.flags.size)
|
||||
assert_equal("Unapproved in three days after returning to moderation queue", @post.flags.last.reason)
|
||||
end
|
||||
|
||||
should "not prune unexpired appeals" do
|
||||
@post = create(:post, created_at: 4.weeks.ago, is_deleted: true)
|
||||
@appeal = create(:post_appeal, post: @post, created_at: 1.day.ago)
|
||||
PostPruner.prune!
|
||||
|
||||
assert_equal(false, @post.reload.is_pending?)
|
||||
assert_equal(false, @post.is_flagged?)
|
||||
assert_equal(true, @post.is_deleted?)
|
||||
assert_equal(true, @appeal.reload.pending?)
|
||||
|
||||
assert_equal(0, @post.flags.size)
|
||||
end
|
||||
|
||||
should "leave the status of old appeals unchanged" do
|
||||
@post = create(:post, created_at: 4.weeks.ago, is_deleted: true)
|
||||
@appeal1 = create(:post_appeal, post: @post, created_at: 3.weeks.ago, status: :succeeded)
|
||||
@appeal2 = create(:post_appeal, post: @post, created_at: 2.weeks.ago, status: :rejected)
|
||||
@appeal3 = create(:post_appeal, post: @post, created_at: 1.weeks.ago, status: :pending)
|
||||
PostPruner.prune!
|
||||
|
||||
assert_equal(true, @post.reload.is_deleted?)
|
||||
assert_equal(false, @post.is_pending?)
|
||||
assert_equal(false, @post.is_flagged?)
|
||||
|
||||
assert_equal(true, @appeal1.reload.succeeded?)
|
||||
assert_equal(true, @appeal2.reload.rejected?)
|
||||
assert_equal(false, @appeal3.reload.pending?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -584,22 +584,26 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
|
||||
flagged = create(:post, is_flagged: true)
|
||||
deleted = create(:post, is_deleted: true)
|
||||
banned = create(:post, is_banned: true)
|
||||
all = [banned, deleted, flagged, pending]
|
||||
appealed = create(:post, is_deleted: true)
|
||||
appeal = create(:post_appeal, post: appealed)
|
||||
all = [appealed, banned, deleted, flagged, pending]
|
||||
|
||||
assert_tag_match([flagged, pending], "status:modqueue")
|
||||
assert_tag_match([appealed, flagged, pending], "status:modqueue")
|
||||
assert_tag_match([pending], "status:pending")
|
||||
assert_tag_match([flagged], "status:flagged")
|
||||
assert_tag_match([deleted], "status:deleted")
|
||||
assert_tag_match([appealed], "status:appealed")
|
||||
assert_tag_match([appealed, deleted], "status:deleted")
|
||||
assert_tag_match([banned], "status:banned")
|
||||
assert_tag_match([banned], "status:active")
|
||||
assert_tag_match([banned], "status:active status:banned")
|
||||
assert_tag_match(all, "status:any")
|
||||
assert_tag_match(all, "status:all")
|
||||
|
||||
assert_tag_match(all - [flagged, pending], "-status:modqueue")
|
||||
assert_tag_match(all - [flagged, pending, appealed], "-status:modqueue")
|
||||
assert_tag_match(all - [pending], "-status:pending")
|
||||
assert_tag_match(all - [flagged], "-status:flagged")
|
||||
assert_tag_match(all - [deleted], "-status:deleted")
|
||||
assert_tag_match(all - [appealed], "-status:appealed")
|
||||
assert_tag_match(all - [deleted, appealed], "-status:deleted")
|
||||
assert_tag_match(all - [banned], "-status:banned")
|
||||
assert_tag_match(all - [banned], "-status:active")
|
||||
|
||||
@@ -611,11 +615,13 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
|
||||
flagged = create(:post, is_flagged: true)
|
||||
pending = create(:post, is_pending: true)
|
||||
disapproved = create(:post, is_pending: true)
|
||||
appealed = create(:post, is_deleted: true)
|
||||
|
||||
create(:post_flag, post: flagged, creator: create(:user, created_at: 2.weeks.ago))
|
||||
create(:post_appeal, post: appealed)
|
||||
create(:post_disapproval, user: CurrentUser.user, post: disapproved, reason: "disinterest")
|
||||
|
||||
assert_tag_match([pending, flagged], "status:unmoderated")
|
||||
assert_tag_match([appealed, pending, flagged], "status:unmoderated")
|
||||
assert_tag_match([disapproved], "-status:unmoderated")
|
||||
end
|
||||
|
||||
|
||||
@@ -513,24 +513,23 @@ class PostTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
context "A status locked post" do
|
||||
setup do
|
||||
@post = FactoryBot.create(:post, is_status_locked: true)
|
||||
end
|
||||
|
||||
should "not allow new flags" do
|
||||
assert_raises(PostFlag::Error) do
|
||||
@post = create(:post, is_status_locked: true)
|
||||
@post.flag!("wrong")
|
||||
end
|
||||
end
|
||||
|
||||
should "not allow new appeals" do
|
||||
@post = create(:post, is_status_locked: true, is_deleted: true)
|
||||
@appeal = build(:post_appeal, post: @post)
|
||||
|
||||
assert_equal(false, @appeal.valid?)
|
||||
assert_equal(["Post is active"], @appeal.errors.full_messages)
|
||||
assert_equal(["Post cannot be appealed"], @appeal.errors.full_messages)
|
||||
end
|
||||
|
||||
should "not allow approval" do
|
||||
@post = create(:post, is_status_locked: true, is_pending: true)
|
||||
approval = @post.approve!
|
||||
assert_includes(approval.errors.full_messages, "Post is locked and cannot be approved")
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ class UploadLimitTest < ActiveSupport::TestCase
|
||||
@post = create(:post, uploader: @user, is_pending: true, created_at: 7.days.ago)
|
||||
assert_equal(1000, @user.reload.upload_points)
|
||||
|
||||
PostPruner.new.prune!
|
||||
PostPruner.prune!
|
||||
assert_equal(967, @user.reload.upload_points)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user