FEATURE: allow admin to delete all posts by a user irrespectively (#14128)
This commit allows admin to delete all posts by a user irrespective of site settings `delete_user_max_post_age` and `delete_all_posts_max`.
This commit is contained in:
parent
f66007ec83
commit
419d71abcb
|
@ -11,7 +11,7 @@ import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { extractError, popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
|
@ -272,73 +272,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
silence() {
|
silence() {
|
||||||
return this.model.silence();
|
return this.model.silence();
|
||||||
},
|
},
|
||||||
deleteAllPosts() {
|
|
||||||
let deletedPosts = 0;
|
|
||||||
let deletedPercentage = 0;
|
|
||||||
const user = this.model;
|
|
||||||
const message = I18n.messageFormat(
|
|
||||||
"admin.user.delete_all_posts_confirm_MF",
|
|
||||||
{
|
|
||||||
POSTS: user.get("post_count"),
|
|
||||||
TOPICS: user.get("topic_count"),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const performDelete = (progressModal) => {
|
|
||||||
this.model
|
|
||||||
.deleteAllPosts()
|
|
||||||
.then(({ posts_deleted }) => {
|
|
||||||
if (posts_deleted === 0) {
|
|
||||||
user.set("post_count", 0);
|
|
||||||
progressModal.send("closeModal");
|
|
||||||
} else {
|
|
||||||
deletedPosts += posts_deleted;
|
|
||||||
deletedPercentage = Math.floor(
|
|
||||||
(deletedPosts * 100) / user.get("post_count")
|
|
||||||
);
|
|
||||||
progressModal.setProperties({
|
|
||||||
deletedPercentage: deletedPercentage,
|
|
||||||
});
|
|
||||||
performDelete(progressModal);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
progressModal.send("closeModal");
|
|
||||||
let error;
|
|
||||||
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
|
|
||||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
|
||||||
error = e.jqXHR.responseJSON.errors[0];
|
|
||||||
}
|
|
||||||
error = error || I18n.t("admin.user.delete_posts_failed");
|
|
||||||
bootbox.alert(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttons = [
|
|
||||||
{
|
|
||||||
label: I18n.t("composer.cancel"),
|
|
||||||
class: "d-modal-cancel",
|
|
||||||
link: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: iconHTML("exclamation-triangle"),
|
|
||||||
label: I18n.t("admin.user.delete_all_posts"),
|
|
||||||
class: "btn btn-danger",
|
|
||||||
callback: () => {
|
|
||||||
const progressModal = openProgressModal();
|
|
||||||
performDelete(progressModal);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const openProgressModal = () => {
|
|
||||||
return showModal("admin-delete-user-posts-progress", {
|
|
||||||
admin: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
bootbox.dialog(message, buttons, { classes: "delete-all-posts" });
|
|
||||||
},
|
|
||||||
|
|
||||||
anonymize() {
|
anonymize() {
|
||||||
const user = this.model;
|
const user = this.model;
|
||||||
|
@ -626,5 +559,50 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showDeletePostsConfirmation() {
|
||||||
|
showModal("admin-delete-posts-confirmation", {
|
||||||
|
admin: true,
|
||||||
|
model: this.model,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAllPosts() {
|
||||||
|
let deletedPosts = 0;
|
||||||
|
let deletedPercentage = 0;
|
||||||
|
const user = this.model;
|
||||||
|
|
||||||
|
const performDelete = (progressModal) => {
|
||||||
|
this.model
|
||||||
|
.deleteAllPosts()
|
||||||
|
.then(({ posts_deleted }) => {
|
||||||
|
if (posts_deleted === 0) {
|
||||||
|
user.set("post_count", 0);
|
||||||
|
progressModal.send("closeModal");
|
||||||
|
} else {
|
||||||
|
deletedPosts += posts_deleted;
|
||||||
|
deletedPercentage = Math.floor(
|
||||||
|
(deletedPosts * 100) / user.get("post_count")
|
||||||
|
);
|
||||||
|
progressModal.setProperties({
|
||||||
|
deletedPercentage: deletedPercentage,
|
||||||
|
});
|
||||||
|
performDelete(progressModal);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
progressModal.send("closeModal");
|
||||||
|
let error;
|
||||||
|
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
|
||||||
|
error = extractError(e) || I18n.t("admin.user.delete_posts_failed");
|
||||||
|
bootbox.alert(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressModal = showModal("admin-delete-user-posts-progress", {
|
||||||
|
admin: true,
|
||||||
|
});
|
||||||
|
performDelete(progressModal);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import Controller, { inject as controller } from "@ember/controller";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { alias } from "@ember/object/computed";
|
||||||
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
export default Controller.extend(ModalFunctionality, {
|
||||||
|
adminUserIndex: controller(),
|
||||||
|
username: alias("model.username"),
|
||||||
|
postCount: alias("model.post_count"),
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
this.set("value", null);
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed("username", "postCount")
|
||||||
|
text(username, postCount) {
|
||||||
|
return I18n.t(`admin.user.delete_posts.confirmation.text`, {
|
||||||
|
username,
|
||||||
|
postCount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed("username")
|
||||||
|
deleteButtonText(username) {
|
||||||
|
return I18n.t(`admin.user.delete_posts.confirmation.delete`, {
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed("value", "text")
|
||||||
|
deleteDisabled(value, text) {
|
||||||
|
return !value || text !== value;
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
confirm() {
|
||||||
|
this.adminUserIndex.send("deleteAllPosts");
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
close() {
|
||||||
|
this.send("closeModal");
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div>
|
||||||
|
{{#d-modal-body rawTitle=(i18n "admin.user.delete_posts.confirmation.title" username=username)}}
|
||||||
|
<p>{{html-safe (i18n "admin.user.delete_posts.confirmation.description" username=username post_count=postCount text=text)}}</p>
|
||||||
|
{{input type="text" value=value}}
|
||||||
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{d-button
|
||||||
|
class="btn-danger"
|
||||||
|
action=(action "confirm")
|
||||||
|
icon="trash-alt"
|
||||||
|
disabled=deleteDisabled
|
||||||
|
translatedLabel=deleteButtonText
|
||||||
|
}}
|
||||||
|
{{d-button
|
||||||
|
action=(action "close")
|
||||||
|
label="admin.user.delete_posts.confirmation.cancel"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
{{#d-modal-body title="admin.user.delete_posts.progress.title" dismissable=false}}
|
{{#d-modal-body title="admin.user.delete_posts.progress.title" dismissable=false}}
|
||||||
<p>{{I18n "admin.user.delete_posts_progress"}}</p>
|
<p>{{I18n "admin.user.delete_posts.progress.description"}}</p>
|
||||||
<div class="progress-bar"><span style={{html-safe (concat "width: " deletedPercentage "%")}}></span></div>
|
<div class="progress-bar"><span style={{html-safe (concat "width: " deletedPercentage "%")}}></span></div>
|
||||||
{{/d-modal-body}}
|
{{/d-modal-body}}
|
||||||
|
|
|
@ -599,9 +599,9 @@
|
||||||
{{#if model.post_count}}
|
{{#if model.post_count}}
|
||||||
{{d-button
|
{{d-button
|
||||||
class="btn-danger"
|
class="btn-danger"
|
||||||
action=(action "deleteAllPosts")
|
action=(action "showDeletePostsConfirmation")
|
||||||
icon="far-trash-alt"
|
icon="far-trash-alt"
|
||||||
label="admin.user.delete_all_posts"}}
|
label="admin.user.delete_posts.button"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{deleteAllPostsExplanation}}
|
{{deleteAllPostsExplanation}}
|
||||||
|
|
|
@ -4878,8 +4878,7 @@ en:
|
||||||
silence_message_placeholder: "(leave blank to send default message)"
|
silence_message_placeholder: "(leave blank to send default message)"
|
||||||
suspended_until: "(until %{until})"
|
suspended_until: "(until %{until})"
|
||||||
cant_suspend: "This user cannot be suspended."
|
cant_suspend: "This user cannot be suspended."
|
||||||
delete_all_posts: "Delete all posts"
|
|
||||||
delete_posts_progress: "Deleting posts..."
|
|
||||||
delete_posts_failed: "There was a problem deleting the posts."
|
delete_posts_failed: "There was a problem deleting the posts."
|
||||||
post_edits: "Post Edits"
|
post_edits: "Post Edits"
|
||||||
view_edits: "View Edits"
|
view_edits: "View Edits"
|
||||||
|
@ -4948,8 +4947,22 @@ en:
|
||||||
anonymize_failed: "There was a problem anonymizing the account."
|
anonymize_failed: "There was a problem anonymizing the account."
|
||||||
delete: "Delete User"
|
delete: "Delete User"
|
||||||
delete_posts:
|
delete_posts:
|
||||||
|
button: "Delete all posts"
|
||||||
progress:
|
progress:
|
||||||
title: "Progress of deleting posts"
|
title: "Progress of deleting posts"
|
||||||
|
description: "Deleting posts..."
|
||||||
|
confirmation:
|
||||||
|
title: "Delete all posts by @%{username}"
|
||||||
|
description: |
|
||||||
|
<p>Are you sure you would like to delete <b>%{post_count}</b> posts by @%{username}?
|
||||||
|
|
||||||
|
<p><b>This can not be undone!</b></p>
|
||||||
|
|
||||||
|
<p>To continue type: <code>%{text}</code></p>
|
||||||
|
|
||||||
|
text: "delete posts by @%{username}"
|
||||||
|
delete: "Delete posts by @%{username}"
|
||||||
|
cancel: "Cancel"
|
||||||
merge:
|
merge:
|
||||||
button: "Merge"
|
button: "Merge"
|
||||||
prompt:
|
prompt:
|
||||||
|
|
|
@ -100,8 +100,9 @@ module PostGuardian
|
||||||
is_staff? &&
|
is_staff? &&
|
||||||
user &&
|
user &&
|
||||||
!user.admin? &&
|
!user.admin? &&
|
||||||
(user.first_post_created_at.nil? || user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago) &&
|
(is_admin? ||
|
||||||
user.post_count <= SiteSetting.delete_all_posts_max.to_i
|
((user.first_post_created_at.nil? || user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago) &&
|
||||||
|
user.post_count <= SiteSetting.delete_all_posts_max.to_i))
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_create_post?(parent)
|
def can_create_post?(parent)
|
||||||
|
|
|
@ -2512,7 +2512,9 @@ describe Guardian do
|
||||||
expect(Guardian.new(user).can_delete_all_posts?(coding_horror)).to be_falsey
|
expect(Guardian.new(user).can_delete_all_posts?(coding_horror)).to be_falsey
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples "can_delete_all_posts examples" do
|
context "for moderators" do
|
||||||
|
let(:actor) { moderator }
|
||||||
|
|
||||||
it "is true if user has no posts" do
|
it "is true if user has no posts" do
|
||||||
SiteSetting.delete_user_max_post_age = 10
|
SiteSetting.delete_user_max_post_age = 10
|
||||||
expect(Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago))).to be_truthy
|
expect(Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago))).to be_truthy
|
||||||
|
@ -2551,14 +2553,45 @@ describe Guardian do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "for moderators" do
|
|
||||||
let(:actor) { moderator }
|
|
||||||
include_examples "can_delete_all_posts examples"
|
|
||||||
end
|
|
||||||
|
|
||||||
context "for admins" do
|
context "for admins" do
|
||||||
let(:actor) { admin }
|
let(:actor) { admin }
|
||||||
include_examples "can_delete_all_posts examples"
|
|
||||||
|
it "is true if user has no posts" do
|
||||||
|
SiteSetting.delete_user_max_post_age = 10
|
||||||
|
expect(Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago))).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is true if user's first post is newer than delete_user_max_post_age days old" do
|
||||||
|
user = Fabricate(:user, created_at: 100.days.ago)
|
||||||
|
user.stubs(:first_post_created_at).returns(9.days.ago)
|
||||||
|
SiteSetting.delete_user_max_post_age = 10
|
||||||
|
expect(Guardian.new(actor).can_delete_all_posts?(user)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is true if user's first post is older than delete_user_max_post_age days old" do
|
||||||
|
user = Fabricate(:user, created_at: 100.days.ago)
|
||||||
|
user.stubs(:first_post_created_at).returns(11.days.ago)
|
||||||
|
SiteSetting.delete_user_max_post_age = 10
|
||||||
|
expect(Guardian.new(actor).can_delete_all_posts?(user)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is false if user is an admin" do
|
||||||
|
expect(Guardian.new(actor).can_delete_all_posts?(admin)).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is true if number of posts is small" do
|
||||||
|
u = Fabricate(:user, created_at: 1.day.ago)
|
||||||
|
u.stubs(:post_count).returns(1)
|
||||||
|
SiteSetting.delete_all_posts_max = 10
|
||||||
|
expect(Guardian.new(actor).can_delete_all_posts?(u)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is true if number of posts is not small" do
|
||||||
|
u = Fabricate(:user, created_at: 1.day.ago)
|
||||||
|
u.stubs(:post_count).returns(11)
|
||||||
|
SiteSetting.delete_all_posts_max = 10
|
||||||
|
expect(Guardian.new(actor).can_delete_all_posts?(u)).to be_truthy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue