From e25638dab0d4b98f99c8fe8976ccaae8f4fb9db3 Mon Sep 17 00:00:00 2001 From: Neil Lalonde <neillalonde@gmail.com> Date: Wed, 24 Jul 2013 13:48:55 -0400 Subject: [PATCH] add a way to delete posts and topics when deleting a user with UserDestroyer --- app/controllers/admin/users_controller.rb | 12 ++- lib/guardian.rb | 4 +- lib/user_destroyer.rb | 10 +- spec/components/guardian_spec.rb | 6 +- spec/components/user_destroyer_spec.rb | 97 +++++++++++-------- .../admin/users_controller_spec.rb | 7 ++ 6 files changed, 87 insertions(+), 49 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 1fad23fe2..4c5dd3ea5 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -117,10 +117,14 @@ class Admin::UsersController < Admin::AdminController def destroy user = User.where(id: params[:id]).first guardian.ensure_can_delete_user!(user) - if UserDestroyer.new(current_user).destroy(user) - render json: {deleted: true} - else - render json: {deleted: false, user: AdminDetailedUserSerializer.new(user, root: false).as_json} + begin + if UserDestroyer.new(current_user).destroy(user, params.slice(:delete_posts)) + render json: {deleted: true} + else + render json: {deleted: false, user: AdminDetailedUserSerializer.new(user, root: false).as_json} + end + rescue UserDestroyer::PostsExistError + raise Discourse::InvalidAccess.new("User #{user.username} has #{user.post_count} posts, so can't be deleted.") end end diff --git a/lib/guardian.rb b/lib/guardian.rb index 33a239cca..2030a572c 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -150,8 +150,8 @@ class Guardian user && is_staff? end - def can_delete_user?(user_to_delete) - can_administer?(user_to_delete) && user_to_delete.post_count <= 0 + def can_delete_user?(user) + can_administer?(user) end # Can we see who acted on a post in a particular way? diff --git a/lib/user_destroyer.rb b/lib/user_destroyer.rb index e23f60054..be949bea1 100644 --- a/lib/user_destroyer.rb +++ b/lib/user_destroyer.rb @@ -11,10 +11,16 @@ class UserDestroyer # Returns false if the user failed to be deleted. # Returns a frozen instance of the User if the delete succeeded. - def destroy(user) + def destroy(user, opts={}) raise Discourse::InvalidParameters.new('user is nil') unless user and user.is_a?(User) - raise PostsExistError if user.post_count != 0 + raise PostsExistError if !opts[:delete_posts] && user.post_count != 0 User.transaction do + if opts[:delete_posts] + user.posts.each do |post| + PostDestroyer.new(@admin, post).destroy + end + raise PostsExistError if user.reload.post_count != 0 + end user.destroy.tap do |u| if u Post.with_deleted.where(user_id: user.id).update_all("nuked_user = true") diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 1758f0141..f69e9d0ae 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -1008,9 +1008,9 @@ describe Guardian do end context "for admins" do - it "is false if user has posts" do - Fabricate(:post, user: user) - Guardian.new(admin).can_delete_user?(user).should be_false + it "is true if user has posts" do # UserDestroyer is responsible for checking for posts + user.stubs(:post_count).returns(1) + Guardian.new(admin).can_delete_user?(user).should be_true end it "is true if user has no posts" do diff --git a/spec/components/user_destroyer_spec.rb b/spec/components/user_destroyer_spec.rb index 8d17fc685..c23cb7c27 100644 --- a/spec/components/user_destroyer_spec.rb +++ b/spec/components/user_destroyer_spec.rb @@ -35,8 +35,6 @@ describe UserDestroyer do @user = Fabricate(:user) end - subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) } - it 'raises an error when user is nil' do expect { UserDestroyer.new(@admin).destroy(nil) }.to raise_error(Discourse::InvalidParameters) end @@ -45,58 +43,79 @@ describe UserDestroyer do expect { UserDestroyer.new(@admin).destroy('nothing') }.to raise_error(Discourse::InvalidParameters) end - context 'user has posts' do - before do - Fabricate(:post, user: @user) + shared_examples "successfully destroy a user" do + it 'should delete the user' do + expect { destroy }.to change { User.count }.by(-1) end - it 'should not delete the user' do - expect { destroy rescue nil }.to_not change { User.count } + it 'should return the deleted user record' do + return_value = destroy + return_value.should == @user + return_value.should be_destroyed end - it 'should raise an error' do - expect { destroy }.to raise_error( UserDestroyer::PostsExistError ) + it 'should log the action' do + StaffActionLogger.any_instance.expects(:log_user_deletion).with(@user).once + destroy end - it 'should not log the action' do - StaffActionLogger.any_instance.expects(:log_user_deletion).never - destroy rescue nil + it 'should unregister the nickname as the discourse hub if hub integration is enabled' do + SiteSetting.stubs(:call_discourse_hub?).returns(true) + DiscourseHub.expects(:unregister_nickname).with(@user.username) + destroy end - it 'should not unregister the user at the discourse hub' do + it 'should not try to unregister the nickname as the discourse hub if hub integration is disabled' do + SiteSetting.stubs(:call_discourse_hub?).returns(false) DiscourseHub.expects(:unregister_nickname).never - destroy rescue nil + destroy + end + end + + context 'user has posts' do + let!(:post) { Fabricate(:post, user: @user) } + + context "delete_posts is false" do + subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) } + + it 'should not delete the user' do + expect { destroy rescue nil }.to_not change { User.count } + end + + it 'should raise an error' do + expect { destroy }.to raise_error( UserDestroyer::PostsExistError ) + end + + it 'should not log the action' do + StaffActionLogger.any_instance.expects(:log_user_deletion).never + destroy rescue nil + end + + it 'should not unregister the user at the discourse hub' do + DiscourseHub.expects(:unregister_nickname).never + destroy rescue nil + end + end + + context "delete_posts is true" do + subject(:destroy) { UserDestroyer.new(@admin).destroy(@user, delete_posts: true) } + + include_examples "successfully destroy a user" + + it "deletes the posts" do + destroy + post.reload.deleted_at.should_not be_nil + post.nuked_user.should be_true + end end end context 'user has no posts' do context 'and destroy succeeds' do - it 'should delete the user' do - expect { destroy }.to change { User.count }.by(-1) - end - it 'should return the deleted user record' do - return_value = destroy - return_value.should == @user - return_value.should be_destroyed - end + subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) } - it 'should log the action' do - StaffActionLogger.any_instance.expects(:log_user_deletion).with(@user).once - destroy - end - - it 'should unregister the nickname as the discourse hub if hub integration is enabled' do - SiteSetting.stubs(:call_discourse_hub?).returns(true) - DiscourseHub.expects(:unregister_nickname).with(@user.username) - destroy - end - - it 'should not try to unregister the nickname as the discourse hub if hub integration is disabled' do - SiteSetting.stubs(:call_discourse_hub?).returns(false) - DiscourseHub.expects(:unregister_nickname).never - destroy - end + include_examples "successfully destroy a user" it "should mark the user's deleted posts as belonging to a nuked user" do post = Fabricate(:post, user: @user, deleted_at: 1.hour.ago) @@ -106,6 +125,8 @@ describe UserDestroyer do end context 'and destroy fails' do + subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) } + before do @user.stubs(:destroy).returns(false) end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 454041934..20bf3d592 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -218,6 +218,13 @@ describe Admin::UsersController do response.should be_forbidden end + it "doesn't return an error if the user has posts and delete_posts == true" do + Fabricate(:post, user: @delete_me) + UserDestroyer.any_instance.expects(:destroy).with(@delete_me, has_entry('delete_posts' => true)).returns(true) + xhr :delete, :destroy, id: @delete_me.id, delete_posts: true + response.should be_success + end + it "deletes the user record" do UserDestroyer.any_instance.expects(:destroy).returns(true) xhr :delete, :destroy, id: @delete_me.id