FEATURE: Allow admins to permanently delete revisions (#19913)

# Context
This PR introduces the ability to permanently delete revisions from a post while maintaining the changes implemented by the revisions.
Additional Context: /t/90301

# Functionality
In the case a staff member wants to _remove the visual cue_ that a post has been edited eg.

<img width="86" alt="Screenshot 2023-01-18 at 2 59 12 PM" src="https://user-images.githubusercontent.com/50783505/213293333-9c881229-ab18-4591-b39b-e3419a67907d.png">

while maintaining the changes made in the edits, they can enable the (hidden) site setting of `can_permanently_delete`.
When this is enabled, after _hiding_ the revisions

<img width="149" alt="Screenshot 2023-01-19 at 1 53 35 PM" src="https://user-images.githubusercontent.com/50783505/213546080-2a9e9c55-b3ef-428e-a93d-1b6ba287dfae.png">

there will be an additional button in the history modal to <kbd>Delete revisions</kbd> on a post.

<img width="997" alt="Screenshot 2023-01-19 at 1 49 51 PM" src="https://user-images.githubusercontent.com/50783505/213546333-49042558-50ab-4724-9da7-08bacc68d38d.png">

Since this action is permanent, we display a confirmation dialog prior to triggering the destroy call

<img width="722" alt="Screenshot 2023-01-19 at 1 55 59 PM" src="https://user-images.githubusercontent.com/50783505/213546487-96ea6e89-ac49-4892-b4b0-28996e3c867f.png">

Once confirmed the history modal will close and the post will `rebake` to display an _unedited_ post.

<img width="868" alt="Screenshot 2023-01-19 at 1 56 35 PM" src="https://user-images.githubusercontent.com/50783505/213546608-d6436717-8484-4132-a1a8-b7a348d92728.png">
 
see that there is not a visual que for _revision have been made on this post_ for a post that **HAS** been edited. In addition to this, a user history log for `purge_post_revisions` will be added for each action completed.

# Limits
- Admins are rate limited to 20 posts per minute
This commit is contained in:
Isaac Janzen 2023-01-19 15:09:01 -06:00 committed by GitHub
parent 2fb2b0a538
commit 292d3677e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 158 additions and 2 deletions

View File

@ -114,6 +114,17 @@ export default Controller.extend(ModalFunctionality, {
);
},
permanentlyDeleteRevisions(postId) {
this.dialog.yesNoConfirm({
message: I18n.t("post.revisions.controls.destroy_confirm"),
didConfirm: () => {
Post.permanentlyDeleteRevisions(postId).then(() => {
this.send("closeModal");
});
},
});
},
show(postId, postVersion) {
Post.showRevision(postId, postVersion).then(() =>
this.refresh(postId, postVersion)
@ -162,6 +173,7 @@ export default Controller.extend(ModalFunctionality, {
},
displayRevisions: gt("model.version_count", 2),
displayGoToFirst: propertyGreaterThan(
"model.current_revision",
"model.first_revision"
@ -215,6 +227,15 @@ export default Controller.extend(ModalFunctionality, {
return this.currentUser && this.currentUser.get("staff");
},
@discourseComputed("model.previous_hidden")
displayPermanentlyDeleteButton(previousHidden) {
return (
this.siteSettings.can_permanently_delete &&
this.currentUser?.staff &&
previousHidden
);
},
isEitherRevisionHidden: or("model.previous_hidden", "model.current_hidden"),
@discourseComputed(
@ -352,6 +373,9 @@ export default Controller.extend(ModalFunctionality, {
hideVersion() {
this.hide(this.get("model.post_id"), this.get("model.current_revision"));
},
permanentlyDeleteVersions() {
this.permanentlyDeleteRevisions(this.get("model.post_id"));
},
showVersion() {
this.show(this.get("model.post_id"), this.get("model.current_revision"));
},

View File

@ -461,6 +461,12 @@ Post.reopenClass({
});
},
permanentlyDeleteRevisions(postId) {
return ajax(`/posts/${postId}/revisions/permanently_delete`, {
type: "DELETE",
});
},
showRevision(postId, version) {
return ajax(`/posts/${postId}/revisions/${version}/show`, {
type: "PUT",

View File

@ -250,6 +250,16 @@
@disabled={{this.loading}}
/>
{{/if}}
{{#if this.displayPermanentlyDeleteButton}}
<DButton
@action={{action "permanentlyDeleteVersions"}}
@icon="far-trash-alt"
@label="post.revisions.controls.destroy"
@class="btn-danger"
@disabled={{this.loading}}
/>
{{/if}}
</div>
</div>
{{/if}}

View File

@ -466,6 +466,33 @@ class PostsController < ApplicationController
render body: nil
end
def permanently_delete_revisions
guardian.ensure_can_permanently_delete_post_revisions!
post = find_post_from_params
raise Discourse::InvalidParameters.new(:post) if post.blank?
raise Discourse::NotFound unless post.revisions.present?
RateLimiter.new(
current_user,
"admin_permanently_delete_post_revisions",
20,
1.minute,
apply_limit_to_staff: true,
).performed!
ActiveRecord::Base.transaction do
updated_at = Time.zone.now
post.revisions.destroy_all
post.update(version: 1, public_version: 1, last_version_at: updated_at)
StaffActionLogger.new(current_user).log_permanently_delete_post_revisions(post)
end
post.rebake!
render body: nil
end
def show_revision
post_revision = find_post_revision_from_params
guardian.ensure_can_show_post_revision!(post_revision)

View File

@ -119,6 +119,7 @@ class UserHistory < ActiveRecord::Base
watched_word_create: 97,
watched_word_destroy: 98,
delete_group: 99,
permanently_delete_post_revisions: 100,
)
end
@ -213,6 +214,7 @@ class UserHistory < ActiveRecord::Base
watched_word_create
watched_word_destroy
delete_group
permanently_delete_post_revisions
]
end

View File

@ -954,6 +954,16 @@ class StaffActionLogger
)
end
def log_permanently_delete_post_revisions(post)
raise Discourse::InvalidParameters.new(:post) if post.nil?
UserHistory.create!(
action: UserHistory.actions[:permanently_delete_post_revisions],
acting_user_id: @admin.id,
post_id: post.id,
)
end
private
def get_changes(changes)

View File

@ -1121,7 +1121,7 @@ en:
perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings."
disable: "Disable Notifications"
enable: "Enable Notifications"
each_browser_note: 'Note: You have to change this setting on every browser you use. All notifications will be disabled if you pause notifications from user menu, regardless of this setting.'
each_browser_note: "Note: You have to change this setting on every browser you use. All notifications will be disabled if you pause notifications from user menu, regardless of this setting."
consent_prompt: "Do you want live notifications when people reply to your posts?"
dismiss: "Dismiss"
dismiss_notifications: "Dismiss All"
@ -3548,6 +3548,8 @@ en:
last: "Last revision"
hide: "Hide revision"
show: "Show revision"
destroy: "Delete revisions"
destroy_confirm: "Are you sure you want to delete all of the revisions on this post? This action is permanent."
revert: "Revert to revision %{revision}"
edit_wiki: "Edit Wiki"
edit_post: "Edit Post"

View File

@ -1082,6 +1082,7 @@ Discourse::Application.routes.draw do
put "revisions/:revision/hide" => "posts#hide_revision", :constraints => { revision: /\d+/ }
put "revisions/:revision/show" => "posts#show_revision", :constraints => { revision: /\d+/ }
put "revisions/:revision/revert" => "posts#revert", :constraints => { revision: /\d+/ }
delete "revisions/permanently_delete" => "posts#permanently_delete_revisions"
put "recover"
collection do
delete "destroy_many"

View File

@ -13,6 +13,10 @@ module PostRevisionGuardian
is_staff?
end
def can_permanently_delete_post_revisions?
is_staff? && SiteSetting.can_permanently_delete
end
def can_show_post_revision?(post_revision)
is_staff?
end

View File

@ -2049,6 +2049,76 @@ RSpec.describe PostsController do
end
end
describe "#permanently_delete_revisions" do
before { SiteSetting.can_permanently_delete = true }
fab!(:post) do
Fabricate(
:post,
user: Fabricate(:user),
raw: "Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex",
)
end
fab!(:post_with_no_revisions) do
Fabricate(
:post,
user: Fabricate(:user),
raw: "Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex",
)
end
fab!(:post_revision) { Fabricate(:post_revision, post: post) }
fab!(:post_revision_2) { Fabricate(:post_revision, post: post) }
let(:post_id) { post.id }
describe "when logged in as a regular user" do
it "does not delete revisions" do
sign_in(user)
delete "/posts/#{post_id}/revisions/permanently_delete.json"
expect(response).to_not be_successful
end
end
describe "when logged in as staff" do
before { sign_in(admin) }
it "fails when post record is not found" do
delete "/posts/#{post_id + 1}/revisions/permanently_delete.json"
expect(response).to_not be_successful
end
it "fails when no post revisions are found" do
delete "/posts/#{post_with_no_revisions.id}/revisions/permanently_delete.json"
expect(response).to_not be_successful
end
it "fails when 'can_permanently_delete' setting is false" do
SiteSetting.can_permanently_delete = false
delete "/posts/#{post_id}/revisions/permanently_delete.json"
expect(response).to_not be_successful
end
it "permanently deletes revisions from post and adds a staff log" do
delete "/posts/#{post_id}/revisions/permanently_delete.json"
expect(response.status).to eq(200)
# It creates a staff log
logs =
UserHistory.find_by(
action: UserHistory.actions[:permanently_delete_post_revisions],
acting_user_id: admin.id,
post_id: post_id,
)
expect(logs).to be_present
# ensure post revisions are deleted
expect(PostRevision.where(post: post)).to eq([])
end
end
end
describe "#revert" do
include_examples "action requires login", :put, "/posts/123/revisions/2/revert.json"