FEATURE: Allow admins to permanently delete posts and topics (#14406)

FEATURE: Allow admins to permanently delete posts and topics (#14406)

Sometimes administrators want to permanently delete posts and topics from the database. To make sure that this is done for a good reasons, administrators can do this only after one minute has passed since the post was deleted or immediately if another administrator does it.

diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js
index e6556e9..a0e0182 100644
--- a/app/assets/javascripts/discourse/app/controllers/topic.js
+++ b/app/assets/javascripts/discourse/app/controllers/topic.js
@@ -595,9 +595,9 @@ export default Controller.extend(bufferedProperty("model"), {
       post.get("post_number") === 1 ? this.recoverTopic() : post.recover();
     },
 
-    deletePost(post) {
+    deletePost(post, opts) {
       if (post.get("post_number") === 1) {
-        return this.deleteTopic();
+        return this.deleteTopic(opts);
       } else if (!post.can_delete) {
         return false;
       }
@@ -611,7 +611,7 @@ export default Controller.extend(bufferedProperty("model"), {
         ajax(`/posts/${post.id}/reply-ids.json`).then((replies) => {
           if (replies.length === 0) {
             return post
-              .destroy(user)
+              .destroy(user, opts)
               .then(refresh)
               .catch((error) => {
                 popupAjaxError(error);
@@ -630,7 +630,7 @@ export default Controller.extend(bufferedProperty("model"), {
             label: I18n.t("post.controls.delete_replies.just_the_post"),
             callback() {
               post
-                .destroy(user)
+                .destroy(user, opts)
                 .then(refresh)
                 .catch((error) => {
                   popupAjaxError(error);
@@ -685,7 +685,7 @@ export default Controller.extend(bufferedProperty("model"), {
         });
       } else {
         return post
-          .destroy(user)
+          .destroy(user, opts)
           .then(refresh)
           .catch((error) => {
             popupAjaxError(error);
@@ -694,6 +694,19 @@ export default Controller.extend(bufferedProperty("model"), {
       }
     },
 
+    permanentlyDeletePost(post) {
+      return bootbox.confirm(
+        I18n.t("post.controls.permanently_delete_confirmation"),
+        I18n.t("no_value"),
+        I18n.t("yes_value"),
+        (result) => {
+          if (result) {
+            this.send("deletePost", post, { force_destroy: true });
+          }
+        }
+      );
+    },
+
     editPost(post) {
       if (!this.currentUser) {
         return bootbox.alert(I18n.t("post.controls.edit_anonymous"));
@@ -1497,13 +1510,13 @@ export default Controller.extend(bufferedProperty("model"), {
     this.model.recover();
   },
 
-  deleteTopic() {
+  deleteTopic(opts) {
     if (
       this.model.views > this.siteSettings.min_topic_views_for_delete_confirm
     ) {
       this.deleteTopicModal();
     } else {
-      this.model.destroy(this.currentUser);
+      this.model.destroy(this.currentUser, opts);
     }
   },
 
diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js
index 8f61645..d1463a9 100644
--- a/app/assets/javascripts/discourse/app/lib/transform-post.js
+++ b/app/assets/javascripts/discourse/app/lib/transform-post.js
@@ -52,6 +52,7 @@ export function transformBasicPost(post) {
     created_at: post.created_at,
     updated_at: post.updated_at,
     canDelete: post.can_delete,
+    canPermanentlyDelete: post.can_permanently_delete,
     showFlagDelete: false,
     canRecover: post.can_recover,
     canEdit: post.can_edit,
@@ -261,6 +262,7 @@ export default function transformPost(
     postAtts.canRecoverTopic = postAtts.isDeleted && details.can_recover;
     postAtts.canDeleteTopic = !postAtts.isDeleted && details.can_delete;
     postAtts.expandablePost = topic.expandable_first_post;
+    postAtts.canPermanentlyDeleteTopic = details.can_permanently_delete;
 
     // Show a "Flag to delete" message if not staff and you can't
     // otherwise delete it.
diff --git a/app/assets/javascripts/discourse/app/models/post.js b/app/assets/javascripts/discourse/app/models/post.js
index 451a06d..74d94bb 100644
--- a/app/assets/javascripts/discourse/app/models/post.js
+++ b/app/assets/javascripts/discourse/app/models/post.js
@@ -249,10 +249,10 @@ const Post = RestModel.extend({
     }
   },
 
-  destroy(deletedBy) {
+  destroy(deletedBy, opts) {
     return this.setDeletedState(deletedBy).then(() => {
       return ajax("/posts/" + this.id, {
-        data: { context: window.location.pathname },
+        data: { context: window.location.pathname, ...opts },
         type: "DELETE",
       });
     });
diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js
index 262bc8c..eed03af 100644
--- a/app/assets/javascripts/discourse/app/models/topic.js
+++ b/app/assets/javascripts/discourse/app/models/topic.js
@@ -433,9 +433,9 @@ const Topic = RestModel.extend({
   },
 
   // Delete this topic
-  destroy(deleted_by) {
+  destroy(deleted_by, opts) {
     return ajax(`/t/${this.id}`, {
-      data: { context: window.location.pathname },
+      data: { context: window.location.pathname, ...opts },
       type: "DELETE",
     })
       .then(() => {
diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs
index b1ef0e9..c935e10 100644
--- a/app/assets/javascripts/discourse/app/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/app/templates/topic.hbs
@@ -217,6 +217,7 @@
                 showLogin=(route-action "showLogin")
                 showRawEmail=(route-action "showRawEmail")
                 deletePost=(action "deletePost")
+                permanentlyDeletePost=(action "permanentlyDeletePost")
                 recoverPost=(action "recoverPost")
                 expandHidden=(action "expandHidden")
                 toggleBookmark=(action "toggleBookmark")
diff --git a/app/assets/javascripts/discourse/app/widgets/post-admin-menu.js b/app/assets/javascripts/discourse/app/widgets/post-admin-menu.js
index e6b58b6..68d79f3 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-admin-menu.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-admin-menu.js
@@ -37,6 +37,15 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
     });
   }
 
+  if (attrs.canPermanentlyDelete || attrs.canPermanentlyDeleteTopic) {
+    contents.push({
+      icon: "trash-alt",
+      className: "popup-menu-button permanently-delete",
+      label: "post.controls.permanently_delete",
+      action: "permanentlyDeletePost",
+    });
+  }
+
   if (!attrs.isWhisper && currentUser.staff) {
     const buttonAtts = {
       action: "togglePostType",
diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js
index 5f09aa7..7f4660c 100644
--- a/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js
@@ -649,6 +649,36 @@ discourseModule("Integration | Component | Widget | post", function (hooks) {
     },
   });
 
+  componentTest("permanently delete topic", {
+    template: hbs`{{mount-widget widget="post" args=args permanentlyDeletePost=permanentlyDeletePost}}`,
+    beforeEach() {
+      this.set("args", { canManage: true, canPermanentlyDeleteTopic: true });
+      this.set("permanentlyDeletePost", () => (this.deleted = true));
+    },
+    async test(assert) {
+      await click(".post-menu-area .show-post-admin-menu");
+      await click(".post-admin-menu .permanently-delete");
+      assert.ok(this.deleted);
+      assert.ok(!exists(".post-admin-menu"), "also hides the menu");
+    },
+  });
+
+  componentTest("permanently delete post", {
+    template: hbs`
+      {{mount-widget widget="post" args=args permanentlyDeletePost=permanentlyDeletePost}}
+    `,
+    beforeEach() {
+      this.set("args", { canManage: true, canPermanentlyDelete: true });
+      this.set("permanentlyDeletePost", () => (this.deleted = true));
+    },
+    async test(assert) {

[... diff too long, it was truncated ...]

GitHub sha: c4843fc1c18fc9fb077d61e59615f54c4735bb42

This commit appears in #14406 which was approved by ZogStriP. It was merged by nbianca.

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there: