mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 08:49:06 +08:00
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:
parent
2fb2b0a538
commit
292d3677e9
|
@ -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) {
|
show(postId, postVersion) {
|
||||||
Post.showRevision(postId, postVersion).then(() =>
|
Post.showRevision(postId, postVersion).then(() =>
|
||||||
this.refresh(postId, postVersion)
|
this.refresh(postId, postVersion)
|
||||||
|
@ -162,6 +173,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
},
|
},
|
||||||
|
|
||||||
displayRevisions: gt("model.version_count", 2),
|
displayRevisions: gt("model.version_count", 2),
|
||||||
|
|
||||||
displayGoToFirst: propertyGreaterThan(
|
displayGoToFirst: propertyGreaterThan(
|
||||||
"model.current_revision",
|
"model.current_revision",
|
||||||
"model.first_revision"
|
"model.first_revision"
|
||||||
|
@ -215,6 +227,15 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
return this.currentUser && this.currentUser.get("staff");
|
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"),
|
isEitherRevisionHidden: or("model.previous_hidden", "model.current_hidden"),
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
|
@ -352,6 +373,9 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
hideVersion() {
|
hideVersion() {
|
||||||
this.hide(this.get("model.post_id"), this.get("model.current_revision"));
|
this.hide(this.get("model.post_id"), this.get("model.current_revision"));
|
||||||
},
|
},
|
||||||
|
permanentlyDeleteVersions() {
|
||||||
|
this.permanentlyDeleteRevisions(this.get("model.post_id"));
|
||||||
|
},
|
||||||
showVersion() {
|
showVersion() {
|
||||||
this.show(this.get("model.post_id"), this.get("model.current_revision"));
|
this.show(this.get("model.post_id"), this.get("model.current_revision"));
|
||||||
},
|
},
|
||||||
|
|
|
@ -461,6 +461,12 @@ Post.reopenClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
permanentlyDeleteRevisions(postId) {
|
||||||
|
return ajax(`/posts/${postId}/revisions/permanently_delete`, {
|
||||||
|
type: "DELETE",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
showRevision(postId, version) {
|
showRevision(postId, version) {
|
||||||
return ajax(`/posts/${postId}/revisions/${version}/show`, {
|
return ajax(`/posts/${postId}/revisions/${version}/show`, {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
|
|
|
@ -250,6 +250,16 @@
|
||||||
@disabled={{this.loading}}
|
@disabled={{this.loading}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/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>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -466,6 +466,33 @@ class PostsController < ApplicationController
|
||||||
render body: nil
|
render body: nil
|
||||||
end
|
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
|
def show_revision
|
||||||
post_revision = find_post_revision_from_params
|
post_revision = find_post_revision_from_params
|
||||||
guardian.ensure_can_show_post_revision!(post_revision)
|
guardian.ensure_can_show_post_revision!(post_revision)
|
||||||
|
|
|
@ -119,6 +119,7 @@ class UserHistory < ActiveRecord::Base
|
||||||
watched_word_create: 97,
|
watched_word_create: 97,
|
||||||
watched_word_destroy: 98,
|
watched_word_destroy: 98,
|
||||||
delete_group: 99,
|
delete_group: 99,
|
||||||
|
permanently_delete_post_revisions: 100,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -213,6 +214,7 @@ class UserHistory < ActiveRecord::Base
|
||||||
watched_word_create
|
watched_word_create
|
||||||
watched_word_destroy
|
watched_word_destroy
|
||||||
delete_group
|
delete_group
|
||||||
|
permanently_delete_post_revisions
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -954,6 +954,16 @@ class StaffActionLogger
|
||||||
)
|
)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def get_changes(changes)
|
def get_changes(changes)
|
||||||
|
|
|
@ -1121,7 +1121,7 @@ en:
|
||||||
perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings."
|
perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings."
|
||||||
disable: "Disable Notifications"
|
disable: "Disable Notifications"
|
||||||
enable: "Enable 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?"
|
consent_prompt: "Do you want live notifications when people reply to your posts?"
|
||||||
dismiss: "Dismiss"
|
dismiss: "Dismiss"
|
||||||
dismiss_notifications: "Dismiss All"
|
dismiss_notifications: "Dismiss All"
|
||||||
|
@ -3548,6 +3548,8 @@ en:
|
||||||
last: "Last revision"
|
last: "Last revision"
|
||||||
hide: "Hide revision"
|
hide: "Hide revision"
|
||||||
show: "Show 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}"
|
revert: "Revert to revision %{revision}"
|
||||||
edit_wiki: "Edit Wiki"
|
edit_wiki: "Edit Wiki"
|
||||||
edit_post: "Edit Post"
|
edit_post: "Edit Post"
|
||||||
|
|
|
@ -1082,6 +1082,7 @@ Discourse::Application.routes.draw do
|
||||||
put "revisions/:revision/hide" => "posts#hide_revision", :constraints => { revision: /\d+/ }
|
put "revisions/:revision/hide" => "posts#hide_revision", :constraints => { revision: /\d+/ }
|
||||||
put "revisions/:revision/show" => "posts#show_revision", :constraints => { revision: /\d+/ }
|
put "revisions/:revision/show" => "posts#show_revision", :constraints => { revision: /\d+/ }
|
||||||
put "revisions/:revision/revert" => "posts#revert", :constraints => { revision: /\d+/ }
|
put "revisions/:revision/revert" => "posts#revert", :constraints => { revision: /\d+/ }
|
||||||
|
delete "revisions/permanently_delete" => "posts#permanently_delete_revisions"
|
||||||
put "recover"
|
put "recover"
|
||||||
collection do
|
collection do
|
||||||
delete "destroy_many"
|
delete "destroy_many"
|
||||||
|
|
|
@ -13,6 +13,10 @@ module PostRevisionGuardian
|
||||||
is_staff?
|
is_staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_permanently_delete_post_revisions?
|
||||||
|
is_staff? && SiteSetting.can_permanently_delete
|
||||||
|
end
|
||||||
|
|
||||||
def can_show_post_revision?(post_revision)
|
def can_show_post_revision?(post_revision)
|
||||||
is_staff?
|
is_staff?
|
||||||
end
|
end
|
||||||
|
|
|
@ -2049,6 +2049,76 @@ RSpec.describe PostsController do
|
||||||
end
|
end
|
||||||
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
|
describe "#revert" do
|
||||||
include_examples "action requires login", :put, "/posts/123/revisions/2/revert.json"
|
include_examples "action requires login", :put, "/posts/123/revisions/2/revert.json"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user