FEATURE: Add option to delete all replies of flagged post

This commit is contained in:
Gerhard Schlager 2018-04-20 23:05:51 +02:00 committed by Robin Ward
parent 035312d501
commit ed4c0c4a63
6 changed files with 108 additions and 15 deletions

View File

@ -62,11 +62,74 @@ export default Post.extend({
},
deferFlags(deletePost) {
return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }).catch(popupAjaxError);
const action = () => {
return ajax('/admin/flags/defer/' + this.id, {
type: 'POST', cache: false, data: { delete_post: deletePost }
});
};
if (deletePost && this._hasDeletableReplies()) {
return this._actOnFlagAndDeleteReplies(action);
} else {
return action().catch(popupAjaxError);
}
},
agreeFlags(actionOnPost) {
return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }).catch(popupAjaxError);
const action = () => {
return ajax('/admin/flags/agree/' + this.id, {
type: 'POST', cache: false, data: { action_on_post: actionOnPost }
});
};
if (actionOnPost === 'delete' && this._hasDeletableReplies()) {
return this._actOnFlagAndDeleteReplies(action);
} else {
return action().catch(popupAjaxError);
}
},
_hasDeletableReplies() {
return this.get('post_number') > 1 && this.get('reply_count') > 0;
},
_actOnFlagAndDeleteReplies(action) {
return new Ember.RSVP.Promise((resolve, reject) => {
return ajax(`/posts/${this.id}/reply-ids/all.json`).then(replies => {
const buttons = [];
buttons.push({
label: I18n.t('no_value'),
callback() {
action()
.then(resolve)
.catch(error => {
popupAjaxError(error);
reject();
});
}
});
buttons.push({
label: I18n.t('yes_value'),
class: "btn-danger",
callback() {
Post.deleteMany(replies.map(r => r.id))
.then(action)
.then(resolve)
.catch(error => {
popupAjaxError(error);
reject();
});
}
});
bootbox.dialog(I18n.t("admin.flags.delete_replies", { count: replies.length }), buttons);
}).catch(error => {
popupAjaxError(error);
reject();
});
});
},
postHidden: Ember.computed.alias('hidden'),

View File

@ -259,6 +259,11 @@ class PostsController < ApplicationController
render json: post.reply_ids(guardian).to_json
end
def all_reply_ids
post = find_post_from_params
render json: post.reply_ids(guardian, only_replies_to_single_post: false).to_json
end
def destroy
post = find_post_from_params
RateLimiter.new(current_user, "delete_post", 3, 1.minute).performed! unless current_user.staff?

View File

@ -682,26 +682,39 @@ class Post < ActiveRecord::Base
MAX_REPLY_LEVEL ||= 1000
def reply_ids(guardian = nil)
replies = Post.exec_sql("
def reply_ids(guardian = nil, only_replies_to_single_post: true)
builder = SqlBuilder.new(<<~SQL, Post)
WITH RECURSIVE breadcrumb(id, level) AS (
SELECT :post_id, 0
UNION
SELECT reply_id, level + 1
FROM post_replies, breadcrumb
WHERE post_id = id
AND post_id <> reply_id
AND level < #{MAX_REPLY_LEVEL}
FROM post_replies AS r
JOIN breadcrumb AS b ON (r.post_id = b.id)
WHERE r.post_id <> r.reply_id
AND b.level < :max_reply_level
), breadcrumb_with_count AS (
SELECT id, level, COUNT(*)
FROM post_replies, breadcrumb
WHERE reply_id = id
AND reply_id <> post_id
SELECT
id,
level,
COUNT(*) AS count
FROM post_replies AS r
JOIN breadcrumb AS b ON (r.reply_id = b.id)
WHERE r.reply_id <> r.post_id
GROUP BY id, level
)
SELECT id, level FROM breadcrumb_with_count WHERE level > 0 AND count = 1 ORDER BY id
", post_id: id).to_a
SELECT id, level
FROM breadcrumb_with_count
/*where*/
ORDER BY id
SQL
builder.where("level > 0")
# ignore posts that aren't replies to exactly one post
# for example it skips a post when it contains 2 quotes (which are replies) from different posts
builder.where("count = 1") if only_replies_to_single_post
replies = builder.exec(post_id: id, max_reply_level: MAX_REPLY_LEVEL).to_a
replies.map! { |r| { id: r["id"].to_i, level: r["level"].to_i } }
secured_ids = Post.secured(guardian).where(id: replies.map { |r| r[:id] }).pluck(:id).to_set

View File

@ -2812,6 +2812,9 @@ en:
replies:
one: "[1 reply]"
other: "[%{count} replies]"
delete_replies:
one: "Also delete the %{count} reply to this post?"
other: "Also delete the %{count} replies to this post?"
dispositions:
agreed: "agreed"

View File

@ -451,6 +451,7 @@ Discourse::Application.routes.draw do
get "posts/by_number/:topic_id/:post_number" => "posts#by_number"
get "posts/:id/reply-history" => "posts#reply_history"
get "posts/:id/reply-ids" => "posts#reply_ids"
get "posts/:id/reply-ids/all" => "posts#all_reply_ids"
get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: RouteFormat.username }
get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: RouteFormat.username }

View File

@ -863,6 +863,14 @@ describe Post do
expect(p6.reply_ids).to be_empty # quotes itself
end
it "does not skip any replies" do
expect(p1.reply_ids(only_replies_to_single_post: false)).to eq([{ id: p2.id, level: 1 }, { id: p4.id, level: 2 }, { id: p5.id, level: 3 }, { id: p6.id, level: 2 }])
expect(p2.reply_ids(only_replies_to_single_post: false)).to eq([{ id: p4.id, level: 1 }, { id: p5.id, level: 2 }, { id: p6.id, level: 1 }])
expect(p3.reply_ids(only_replies_to_single_post: false)).to eq([{ id: p5.id, level: 1 }])
expect(p4.reply_ids(only_replies_to_single_post: false)).to eq([{ id: p5.id, level: 1 }])
expect(p5.reply_ids(only_replies_to_single_post: false)).to be_empty # has no replies
expect(p6.reply_ids(only_replies_to_single_post: false)).to be_empty # quotes itself
end
end
describe 'urls' do