mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 23:58:31 -05:00
allow end user to recover a post they delete
automatically delete stubs after 1 day
This commit is contained in:
parent
d68f30c09d
commit
1f3c5cb656
13 changed files with 131 additions and 23 deletions
|
@ -27,12 +27,12 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
deleted: Em.computed.or('deleted_at', 'deletedViaTopic'),
|
deleted: Em.computed.or('deleted_at', 'deletedViaTopic'),
|
||||||
|
|
||||||
postDeletedBy: function() {
|
postDeletedBy: function() {
|
||||||
if (this.get('firstPost')) { return this.get('topic.deleted_by') }
|
if (this.get('firstPost')) { return this.get('topic.deleted_by'); }
|
||||||
return this.get('deleted_by');
|
return this.get('deleted_by');
|
||||||
}.property('firstPost', 'deleted_by', 'topic.deleted_by'),
|
}.property('firstPost', 'deleted_by', 'topic.deleted_by'),
|
||||||
|
|
||||||
postDeletedAt: function() {
|
postDeletedAt: function() {
|
||||||
if (this.get('firstPost')) { return this.get('topic.deleted_at') }
|
if (this.get('firstPost')) { return this.get('topic.deleted_at'); }
|
||||||
return this.get('deleted_at');
|
return this.get('deleted_at');
|
||||||
}.property('firstPost', 'deleted_at', 'topic.deleted_at'),
|
}.property('firstPost', 'deleted_at', 'topic.deleted_at'),
|
||||||
|
|
||||||
|
@ -199,13 +199,23 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
@method recover
|
@method recover
|
||||||
**/
|
**/
|
||||||
recover: function() {
|
recover: function() {
|
||||||
this.setProperties({
|
var post = this;
|
||||||
|
post.setProperties({
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
deleted_by: null,
|
deleted_by: null,
|
||||||
can_delete: true
|
user_deleted: false,
|
||||||
|
can_delete: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return Discourse.ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false });
|
return Discourse.ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false }).then(function(data){
|
||||||
|
post.setProperties({
|
||||||
|
cooked: data.cooked,
|
||||||
|
raw: data.raw,
|
||||||
|
user_deleted: false,
|
||||||
|
can_delete: true,
|
||||||
|
version: data.version
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,7 +236,10 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
cooked: Discourse.Markdown.cook(I18n.t("post.deleted_by_author")),
|
cooked: Discourse.Markdown.cook(I18n.t("post.deleted_by_author")),
|
||||||
can_delete: false,
|
can_delete: false,
|
||||||
version: this.get('version') + 1
|
version: this.get('version') + 1,
|
||||||
|
can_recover: true,
|
||||||
|
can_edit: false,
|
||||||
|
user_deleted: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ Discourse.PostMenuView = Discourse.View.extend({
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// The delete actions target the post iteself
|
// The delete actions target the post iteself
|
||||||
if (post.get('deleted_at')) {
|
if (post.get('deleted_at') || post.get('user_deleted')) {
|
||||||
if (!post.get('can_recover')) { return; }
|
if (!post.get('can_recover')) { return; }
|
||||||
label = "post.controls.undelete";
|
label = "post.controls.undelete";
|
||||||
action = "recover";
|
action = "recover";
|
||||||
|
|
|
@ -12,10 +12,15 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
classNameBindings: ['postTypeClass',
|
classNameBindings: ['postTypeClass',
|
||||||
'selected',
|
'selected',
|
||||||
'post.hidden:hidden',
|
'post.hidden:hidden',
|
||||||
'post.deleted',
|
'addDeletedClass:deleted',
|
||||||
'parentPost:replies-above'],
|
'parentPost:replies-above'],
|
||||||
postBinding: 'content',
|
postBinding: 'content',
|
||||||
|
|
||||||
|
addDeletedClass: function() {
|
||||||
|
var post = this.get('post');
|
||||||
|
return post.get('deleted') || post.get('user_deleted');
|
||||||
|
}.property('post.deleted','post.user_deleted'),
|
||||||
|
|
||||||
postTypeClass: function() {
|
postTypeClass: function() {
|
||||||
return this.get('post.post_type') === Discourse.Site.instance().get('post_types.moderator_action') ? 'moderator' : 'regular';
|
return this.get('post.post_type') === Discourse.Site.instance().get('post_types.moderator_action') ? 'moderator' : 'regular';
|
||||||
}.property('post.post_type'),
|
}.property('post.post_type'),
|
||||||
|
|
|
@ -94,17 +94,13 @@ class PostsController < ApplicationController
|
||||||
@post = Post.where(topic_id: params[:topic_id], post_number: params[:post_number]).first
|
@post = Post.where(topic_id: params[:topic_id], post_number: params[:post_number]).first
|
||||||
guardian.ensure_can_see!(@post)
|
guardian.ensure_can_see!(@post)
|
||||||
@post.revert_to(params[:version].to_i) if params[:version].present?
|
@post.revert_to(params[:version].to_i) if params[:version].present?
|
||||||
post_serializer = PostSerializer.new(@post, scope: guardian, root: false)
|
render_post_json(@post)
|
||||||
post_serializer.add_raw = true
|
|
||||||
render_json_dump(post_serializer)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@post = find_post_from_params
|
@post = find_post_from_params
|
||||||
@post.revert_to(params[:version].to_i) if params[:version].present?
|
@post.revert_to(params[:version].to_i) if params[:version].present?
|
||||||
post_serializer = PostSerializer.new(@post, scope: guardian, root: false)
|
render_post_json(@post)
|
||||||
post_serializer.add_raw = true
|
|
||||||
render_json_dump(post_serializer)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -120,10 +116,11 @@ class PostsController < ApplicationController
|
||||||
def recover
|
def recover
|
||||||
post = find_post_from_params
|
post = find_post_from_params
|
||||||
guardian.ensure_can_recover_post!(post)
|
guardian.ensure_can_recover_post!(post)
|
||||||
post.recover!
|
destroyer = PostDestroyer.new(current_user, post)
|
||||||
post.topic.update_statistics
|
destroyer.recover
|
||||||
|
post.reload
|
||||||
|
|
||||||
render nothing: true
|
render_post_json(post)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy_many
|
def destroy_many
|
||||||
|
@ -188,6 +185,12 @@ class PostsController < ApplicationController
|
||||||
post
|
post
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_post_json(post)
|
||||||
|
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
|
||||||
|
post_serializer.add_raw = true
|
||||||
|
render_json_dump(post_serializer)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_params
|
def create_params
|
||||||
|
|
|
@ -208,6 +208,7 @@ ORDER BY p.created_at desc
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.synchronize_target_topic_ids(post_ids = nil)
|
def self.synchronize_target_topic_ids(post_ids = nil)
|
||||||
|
|
||||||
builder = SqlBuilder.new("UPDATE user_actions
|
builder = SqlBuilder.new("UPDATE user_actions
|
||||||
SET target_topic_id = (select topic_id from posts where posts.id = target_post_id)
|
SET target_topic_id = (select topic_id from posts where posts.id = target_post_id)
|
||||||
/*where*/")
|
/*where*/")
|
||||||
|
|
|
@ -41,7 +41,8 @@ class PostSerializer < BasicPostSerializer
|
||||||
:hidden_reason_id,
|
:hidden_reason_id,
|
||||||
:trust_level,
|
:trust_level,
|
||||||
:deleted_at,
|
:deleted_at,
|
||||||
:deleted_by
|
:deleted_by,
|
||||||
|
:user_deleted
|
||||||
|
|
||||||
|
|
||||||
def moderator?
|
def moderator?
|
||||||
|
|
|
@ -34,5 +34,6 @@ module Clockwork
|
||||||
every(1.day, 'version_check')
|
every(1.day, 'version_check')
|
||||||
every(1.minute, 'clockwork_heartbeat')
|
every(1.minute, 'clockwork_heartbeat')
|
||||||
every(1.minute, 'poll_mailbox')
|
every(1.minute, 'poll_mailbox')
|
||||||
|
every(2.hours, 'destroy_old_deletion_stubs')
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -267,7 +267,7 @@ class Guardian
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_edit_post?(post)
|
def can_edit_post?(post)
|
||||||
is_staff? || (not(post.topic.archived?) && is_my_own?(post))
|
is_staff? || (!post.topic.archived? && is_my_own?(post) && !post.user_deleted &&!post.deleted_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_edit_user?(user)
|
def can_edit_user?(user)
|
||||||
|
@ -291,7 +291,7 @@ class Guardian
|
||||||
|
|
||||||
# Recovery Method
|
# Recovery Method
|
||||||
def can_recover_post?(post)
|
def can_recover_post?(post)
|
||||||
is_staff?
|
is_staff? || (is_my_own?(post) && post.user_deleted && !post.deleted_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_recover_topic?(topic)
|
def can_recover_topic?(topic)
|
||||||
|
|
8
lib/jobs/destroy_old_deletion_stubs.rb
Normal file
8
lib/jobs/destroy_old_deletion_stubs.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module Jobs
|
||||||
|
# various consistency checks
|
||||||
|
class DestroyOldDeletionStubs < Jobs::Base
|
||||||
|
def execute(args)
|
||||||
|
PostDestroyer.destroy_stubs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,6 +4,13 @@
|
||||||
#
|
#
|
||||||
class PostDestroyer
|
class PostDestroyer
|
||||||
|
|
||||||
|
def self.destroy_stubs
|
||||||
|
Post.where(deleted_at: nil, user_deleted: true)
|
||||||
|
.where('updated_at < ? AND post_number > 1', 1.day.ago).each do |post|
|
||||||
|
PostDestroyer.new(Discourse.system_user, post).destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(user, post)
|
def initialize(user, post)
|
||||||
@user, @post = user, post
|
@user, @post = user, post
|
||||||
end
|
end
|
||||||
|
@ -16,6 +23,19 @@ class PostDestroyer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recover
|
||||||
|
if @user.staff? && @post.deleted_at
|
||||||
|
staff_recovered
|
||||||
|
elsif @user.staff? || @user.id == @post.user_id
|
||||||
|
user_recovered
|
||||||
|
end
|
||||||
|
@post.topic.update_statistics
|
||||||
|
end
|
||||||
|
|
||||||
|
def staff_recovered
|
||||||
|
@post.recover!
|
||||||
|
end
|
||||||
|
|
||||||
# When a post is properly deleted. Well, it's still soft deleted, but it will no longer
|
# When a post is properly deleted. Well, it's still soft deleted, but it will no longer
|
||||||
# show up in the topic
|
# show up in the topic
|
||||||
def staff_destroyed
|
def staff_destroyed
|
||||||
|
@ -75,4 +95,12 @@ class PostDestroyer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_recovered
|
||||||
|
Post.transaction do
|
||||||
|
@post.update_column(:user_deleted, false)
|
||||||
|
@post.revise(@user, @post.versions.last.modifications["raw"][0], force_new_version: true)
|
||||||
|
@post.update_flagged_posts_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -485,6 +485,16 @@ describe Guardian do
|
||||||
Guardian.new(post.user).can_edit?(post).should be_true
|
Guardian.new(post.user).can_edit?(post).should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns false if you are trying to edit a post you soft deleted' do
|
||||||
|
post.user_deleted = true
|
||||||
|
Guardian.new(post.user).can_edit?(post).should be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if you are trying to edit a deleted post' do
|
||||||
|
post.deleted_at = 1.day.ago
|
||||||
|
Guardian.new(post.user).can_edit?(post).should be_false
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns false if another regular user tries to edit your post' do
|
it 'returns false if another regular user tries to edit your post' do
|
||||||
Guardian.new(coding_horror).can_edit?(post).should be_false
|
Guardian.new(coding_horror).can_edit?(post).should be_false
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,31 @@ describe PostDestroyer do
|
||||||
let(:moderator) { Fabricate(:moderator) }
|
let(:moderator) { Fabricate(:moderator) }
|
||||||
let(:post) { create_post }
|
let(:post) { create_post }
|
||||||
|
|
||||||
|
describe 'destroy_old_stubs' do
|
||||||
|
it 'destroys stubs for deleted by user posts' do
|
||||||
|
Fabricate(:admin)
|
||||||
|
reply1 = create_post(topic: post.topic)
|
||||||
|
reply2 = create_post(topic: post.topic)
|
||||||
|
reply3 = create_post(topic: post.topic)
|
||||||
|
|
||||||
|
PostDestroyer.new(reply1.user, reply1).destroy
|
||||||
|
PostDestroyer.new(reply2.user, reply2).destroy
|
||||||
|
|
||||||
|
reply2.update_column(:updated_at, 2.days.ago)
|
||||||
|
|
||||||
|
PostDestroyer.destroy_stubs
|
||||||
|
|
||||||
|
reply1.reload
|
||||||
|
reply2.reload
|
||||||
|
reply3.reload
|
||||||
|
|
||||||
|
reply1.deleted_at.should == nil
|
||||||
|
reply2.deleted_at.should_not == nil
|
||||||
|
reply3.deleted_at.should == nil
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'basic destroying' do
|
describe 'basic destroying' do
|
||||||
|
|
||||||
let(:moderator) { Fabricate(:moderator) }
|
let(:moderator) { Fabricate(:moderator) }
|
||||||
|
@ -17,6 +42,7 @@ describe PostDestroyer do
|
||||||
|
|
||||||
context "as the creator of the post" do
|
context "as the creator of the post" do
|
||||||
before do
|
before do
|
||||||
|
@orig = post.cooked
|
||||||
PostDestroyer.new(post.user, post).destroy
|
PostDestroyer.new(post.user, post).destroy
|
||||||
post.reload
|
post.reload
|
||||||
end
|
end
|
||||||
|
@ -24,8 +50,16 @@ describe PostDestroyer do
|
||||||
it "doesn't delete the post" do
|
it "doesn't delete the post" do
|
||||||
post.deleted_at.should be_blank
|
post.deleted_at.should be_blank
|
||||||
post.deleted_by.should be_blank
|
post.deleted_by.should be_blank
|
||||||
|
post.user_deleted.should be_true
|
||||||
post.raw.should == I18n.t('js.post.deleted_by_author')
|
post.raw.should == I18n.t('js.post.deleted_by_author')
|
||||||
post.version.should == 2
|
post.version.should == 2
|
||||||
|
|
||||||
|
# lets try to recover
|
||||||
|
PostDestroyer.new(post.user, post).recover
|
||||||
|
post.reload
|
||||||
|
post.version.should == 3
|
||||||
|
post.user_deleted.should be_false
|
||||||
|
post.cooked.should == @orig
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -124,10 +124,14 @@ describe PostsController do
|
||||||
response.should be_forbidden
|
response.should be_forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calls recover and updates the topic's statistics" do
|
it "recovers a post correctly" do
|
||||||
Post.any_instance.expects(:recover!)
|
topic_id = create_post.topic_id
|
||||||
Topic.any_instance.expects(:update_statistics)
|
post = create_post(topic_id: topic_id)
|
||||||
|
|
||||||
|
PostDestroyer.new(user, post).destroy
|
||||||
xhr :put, :recover, post_id: post.id
|
xhr :put, :recover, post_id: post.id
|
||||||
|
post.reload
|
||||||
|
post.deleted_at.should == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue