diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js index 84cbcbc9681..60713d646b9 100644 --- a/app/assets/javascripts/discourse/models/post.js +++ b/app/assets/javascripts/discourse/models/post.js @@ -27,12 +27,12 @@ Discourse.Post = Discourse.Model.extend({ deleted: Em.computed.or('deleted_at', 'deletedViaTopic'), 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'); }.property('firstPost', 'deleted_by', 'topic.deleted_by'), 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'); }.property('firstPost', 'deleted_at', 'topic.deleted_at'), @@ -199,13 +199,23 @@ Discourse.Post = Discourse.Model.extend({ @method recover **/ recover: function() { - this.setProperties({ + var post = this; + post.setProperties({ deleted_at: 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({ cooked: Discourse.Markdown.cook(I18n.t("post.deleted_by_author")), can_delete: false, - version: this.get('version') + 1 + version: this.get('version') + 1, + can_recover: true, + can_edit: false, + user_deleted: true }); } diff --git a/app/assets/javascripts/discourse/views/post_menu_view.js b/app/assets/javascripts/discourse/views/post_menu_view.js index 7193ceacbaa..0cbd8c27adc 100644 --- a/app/assets/javascripts/discourse/views/post_menu_view.js +++ b/app/assets/javascripts/discourse/views/post_menu_view.js @@ -88,7 +88,7 @@ Discourse.PostMenuView = Discourse.View.extend({ } else { // 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; } label = "post.controls.undelete"; action = "recover"; diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js index 961d5fad116..60a532552d3 100644 --- a/app/assets/javascripts/discourse/views/post_view.js +++ b/app/assets/javascripts/discourse/views/post_view.js @@ -12,10 +12,15 @@ Discourse.PostView = Discourse.View.extend({ classNameBindings: ['postTypeClass', 'selected', 'post.hidden:hidden', - 'post.deleted', + 'addDeletedClass:deleted', 'parentPost:replies-above'], 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() { return this.get('post.post_type') === Discourse.Site.instance().get('post_types.moderator_action') ? 'moderator' : 'regular'; }.property('post.post_type'), diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 3e0d903951e..45081177ba5 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -94,17 +94,13 @@ class PostsController < ApplicationController @post = Post.where(topic_id: params[:topic_id], post_number: params[:post_number]).first guardian.ensure_can_see!(@post) @post.revert_to(params[:version].to_i) if params[:version].present? - post_serializer = PostSerializer.new(@post, scope: guardian, root: false) - post_serializer.add_raw = true - render_json_dump(post_serializer) + render_post_json(@post) end def show @post = find_post_from_params @post.revert_to(params[:version].to_i) if params[:version].present? - post_serializer = PostSerializer.new(@post, scope: guardian, root: false) - post_serializer.add_raw = true - render_json_dump(post_serializer) + render_post_json(@post) end def destroy @@ -120,10 +116,11 @@ class PostsController < ApplicationController def recover post = find_post_from_params guardian.ensure_can_recover_post!(post) - post.recover! - post.topic.update_statistics + destroyer = PostDestroyer.new(current_user, post) + destroyer.recover + post.reload - render nothing: true + render_post_json(post) end def destroy_many @@ -188,6 +185,12 @@ class PostsController < ApplicationController post 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 def create_params diff --git a/app/models/user_action.rb b/app/models/user_action.rb index f971dcfae50..d702656c0de 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -208,6 +208,7 @@ ORDER BY p.created_at desc end def self.synchronize_target_topic_ids(post_ids = nil) + builder = SqlBuilder.new("UPDATE user_actions SET target_topic_id = (select topic_id from posts where posts.id = target_post_id) /*where*/") diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index e13a56ccb83..48e1cef6001 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -41,7 +41,8 @@ class PostSerializer < BasicPostSerializer :hidden_reason_id, :trust_level, :deleted_at, - :deleted_by + :deleted_by, + :user_deleted def moderator? diff --git a/config/clock.rb b/config/clock.rb index e225bc272cb..81fb757d6e2 100644 --- a/config/clock.rb +++ b/config/clock.rb @@ -34,5 +34,6 @@ module Clockwork every(1.day, 'version_check') every(1.minute, 'clockwork_heartbeat') every(1.minute, 'poll_mailbox') + every(2.hours, 'destroy_old_deletion_stubs') end diff --git a/lib/guardian.rb b/lib/guardian.rb index d2c9b093bbb..7c55d4917be 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -267,7 +267,7 @@ class Guardian end 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 def can_edit_user?(user) @@ -291,7 +291,7 @@ class Guardian # Recovery Method def can_recover_post?(post) - is_staff? + is_staff? || (is_my_own?(post) && post.user_deleted && !post.deleted_at) end def can_recover_topic?(topic) diff --git a/lib/jobs/destroy_old_deletion_stubs.rb b/lib/jobs/destroy_old_deletion_stubs.rb new file mode 100644 index 00000000000..a6c3021a27d --- /dev/null +++ b/lib/jobs/destroy_old_deletion_stubs.rb @@ -0,0 +1,8 @@ +module Jobs + # various consistency checks + class DestroyOldDeletionStubs < Jobs::Base + def execute(args) + PostDestroyer.destroy_stubs + end + end +end diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 7f011f5c3fb..82aa4d65831 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -4,6 +4,13 @@ # 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) @user, @post = user, post end @@ -16,6 +23,19 @@ class PostDestroyer 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 # show up in the topic def staff_destroyed @@ -75,4 +95,12 @@ class PostDestroyer 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 diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index ca52bcf451a..947639752e4 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -485,6 +485,16 @@ describe Guardian do Guardian.new(post.user).can_edit?(post).should be_true 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 Guardian.new(coding_horror).can_edit?(post).should be_false end diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 747df0fecba..d38c9d1fc4b 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -10,6 +10,31 @@ describe PostDestroyer do let(:moderator) { Fabricate(:moderator) } 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 let(:moderator) { Fabricate(:moderator) } @@ -17,6 +42,7 @@ describe PostDestroyer do context "as the creator of the post" do before do + @orig = post.cooked PostDestroyer.new(post.user, post).destroy post.reload end @@ -24,8 +50,16 @@ describe PostDestroyer do it "doesn't delete the post" do post.deleted_at.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.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 diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index cb88c55bc28..fa476d0c5e1 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -124,10 +124,14 @@ describe PostsController do response.should be_forbidden end - it "calls recover and updates the topic's statistics" do - Post.any_instance.expects(:recover!) - Topic.any_instance.expects(:update_statistics) + it "recovers a post correctly" do + topic_id = create_post.topic_id + post = create_post(topic_id: topic_id) + + PostDestroyer.new(user, post).destroy xhr :put, :recover, post_id: post.id + post.reload + post.deleted_at.should == nil end end