From 419d71abcb814b14c15918190405428bfb543548 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 25 Aug 2021 10:14:22 +0530 Subject: [PATCH] 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`. --- .../addon/controllers/admin-user-index.js | 114 +++++++----------- .../modals/admin-delete-posts-confirmation.js | 46 +++++++ .../modal/admin-delete-posts-confirmation.hbs | 20 +++ .../admin-delete-user-posts-progress.hbs | 2 +- .../admin/addon/templates/user-index.hbs | 4 +- config/locales/client.en.yml | 17 ++- lib/guardian/post_guardian.rb | 5 +- spec/components/guardian_spec.rb | 47 ++++++-- 8 files changed, 173 insertions(+), 82 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/controllers/modals/admin-delete-posts-confirmation.js create mode 100644 app/assets/javascripts/admin/addon/templates/modal/admin-delete-posts-confirmation.hbs diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js index b9492e30af4..93ed9c306f1 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js @@ -11,7 +11,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import getURL from "discourse-common/lib/get-url"; import { htmlSafe } from "@ember/template"; 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 showModal from "discourse/lib/show-modal"; @@ -272,73 +272,6 @@ export default Controller.extend(CanCheckEmails, { 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() { 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); + }, }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-delete-posts-confirmation.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-delete-posts-confirmation.js new file mode 100644 index 00000000000..106b459f0ce --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-delete-posts-confirmation.js @@ -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"); + }, +}); diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-delete-posts-confirmation.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-delete-posts-confirmation.hbs new file mode 100644 index 00000000000..9c5cb275c3b --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-delete-posts-confirmation.hbs @@ -0,0 +1,20 @@ +
+ {{#d-modal-body rawTitle=(i18n "admin.user.delete_posts.confirmation.title" username=username)}} +

{{html-safe (i18n "admin.user.delete_posts.confirmation.description" username=username post_count=postCount text=text)}}

+ {{input type="text" value=value}} + {{/d-modal-body}} + + +
diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-delete-user-posts-progress.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-delete-user-posts-progress.hbs index 8f381db8471..1f962689428 100644 --- a/app/assets/javascripts/admin/addon/templates/modal/admin-delete-user-posts-progress.hbs +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-delete-user-posts-progress.hbs @@ -1,4 +1,4 @@ {{#d-modal-body title="admin.user.delete_posts.progress.title" dismissable=false}} -

{{I18n "admin.user.delete_posts_progress"}}

+

{{I18n "admin.user.delete_posts.progress.description"}}

{{/d-modal-body}} diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs index 17350dc1f0f..d81aeb21f46 100644 --- a/app/assets/javascripts/admin/addon/templates/user-index.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs @@ -599,9 +599,9 @@ {{#if model.post_count}} {{d-button class="btn-danger" - action=(action "deleteAllPosts") + action=(action "showDeletePostsConfirmation") icon="far-trash-alt" - label="admin.user.delete_all_posts"}} + label="admin.user.delete_posts.button"}} {{/if}} {{else}} {{deleteAllPostsExplanation}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fcbb059ecc5..96aba1a02ce 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4878,8 +4878,7 @@ en: silence_message_placeholder: "(leave blank to send default message)" suspended_until: "(until %{until})" 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." post_edits: "Post Edits" view_edits: "View Edits" @@ -4948,8 +4947,22 @@ en: anonymize_failed: "There was a problem anonymizing the account." delete: "Delete User" delete_posts: + button: "Delete all posts" progress: title: "Progress of deleting posts" + description: "Deleting posts..." + confirmation: + title: "Delete all posts by @%{username}" + description: | +

Are you sure you would like to delete %{post_count} posts by @%{username}? + +

This can not be undone!

+ +

To continue type: %{text}

+ + text: "delete posts by @%{username}" + delete: "Delete posts by @%{username}" + cancel: "Cancel" merge: button: "Merge" prompt: diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index fc3bbeb2d05..6b622a076b0 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -100,8 +100,9 @@ module PostGuardian is_staff? && user && !user.admin? && - (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 + (is_admin? || + ((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 def can_create_post?(parent) diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 9695a98da90..6e329261ea4 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -2512,7 +2512,9 @@ describe Guardian do expect(Guardian.new(user).can_delete_all_posts?(coding_horror)).to be_falsey 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 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 @@ -2551,14 +2553,45 @@ describe Guardian do end end - context "for moderators" do - let(:actor) { moderator } - include_examples "can_delete_all_posts examples" - end - context "for admins" do 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