From 092eeb5ca351a34d8a2892048a3a40099026497b Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Wed, 17 Jul 2019 12:41:13 -0600 Subject: [PATCH] FEATURE: Create a rake task for destroying categories Created a rake task for destroying multiple categories along with any subcategories and topics the belong to those categories. Also created a rake task for listing all of your categories. Refactored existing destroy rake tasks to use new logging method, that allows for puts output in the console but prevents it from showing in the specs. --- app/services/destroy_task.rb | 104 +++++++++++++++++++---------- lib/tasks/categories.rake | 8 +++ lib/tasks/destroy.rake | 33 ++++++--- spec/services/destroy_task_spec.rb | 55 ++++++++++++--- 4 files changed, 148 insertions(+), 52 deletions(-) diff --git a/app/services/destroy_task.rb b/app/services/destroy_task.rb index f7c8a64123d..e5ff8e5789c 100644 --- a/app/services/destroy_task.rb +++ b/app/services/destroy_task.rb @@ -1,83 +1,110 @@ # frozen_string_literal: true -## Because these methods are meant to be called from a rake task -# we are capturing all log output into a log array to return -# to the rake task rather than using `puts` statements. class DestroyTask - def self.destroy_topics(category, parent_category = nil) + + def initialize(io = $stdout) + @io = io + end + + def destroy_topics(category, parent_category = nil, delete_system_topics = false) c = Category.find_by_slug(category, parent_category) - log = [] descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category - return "A category with the slug: #{descriptive_slug} could not be found" if c.nil? - topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1) - log << "There are #{topics.count} topics to delete in #{descriptive_slug} category" + return @io.puts "A category with the slug: #{descriptive_slug} could not be found" if c.nil? + if delete_system_topics + topics = Topic.where(category_id: c.id, pinned_at: nil) + else + topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1) + end + @io.puts "There are #{topics.count} topics to delete in #{descriptive_slug} category" topics.each do |topic| - log << "Deleting #{topic.slug}..." + @io.puts "Deleting #{topic.slug}..." first_post = topic.ordered_posts.first if first_post.nil? - return log << "Topic.ordered_posts.first was nil" + return @io.puts "Topic.ordered_posts.first was nil" end system_user = User.find(-1) - log << PostDestroyer.new(system_user, first_post).destroy + @io.puts PostDestroyer.new(system_user, first_post).destroy end - log end - def self.destroy_topics_all_categories + def destroy_topics_in_category(category_id, delete_system_topics = false) + c = Category.find(category_id) + return @io.puts "A category with the id: #{category_id} could not be found" if c.nil? + if delete_system_topics + topics = Topic.where(category_id: c.id, pinned_at: nil) + else + topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1) + end + @io.puts "There are #{topics.count} topics to delete in #{c.slug} category" + topics.each do |topic| + first_post = topic.ordered_posts.first + return @io.puts "Topic.ordered_posts.first was nil for topic: #{topic.id}" if first_post.nil? + system_user = User.find(-1) + PostDestroyer.new(system_user, first_post).destroy + end + topics = Topic.where(category_id: c.id, pinned_at: nil) + @io.puts "There are #{topics.count} topics that could not be deleted in #{c.slug} category" + end + + def destroy_topics_all_categories categories = Category.all - log = [] categories.each do |c| - log << destroy_topics(c.slug, c.parent_category&.slug) + @io.puts destroy_topics(c.slug, c.parent_category&.slug) end - log end - def self.destroy_private_messages + def destroy_private_messages pms = Topic.where(archetype: "private_message") current_user = User.find(-1) #system - log = [] pms.each do |pm| - log << "Destroying #{pm.slug} pm" + @io.puts "Destroying #{pm.slug} pm" first_post = pm.ordered_posts.first - log << PostDestroyer.new(current_user, first_post).destroy + @io.puts PostDestroyer.new(current_user, first_post).destroy end - log end - def self.destroy_groups + def destroy_category(category_id, destroy_system_topics = false) + c = Category.find_by_id(category_id) + return @io.puts "A category with the id: #{category_id} could not be found" if c.nil? + subcategories = Category.where(parent_category_id: c.id).pluck(:id) + @io.puts "There are #{subcategories.count} subcategories to delete" if subcategories + subcategories.each do |subcategory_id| + s = Category.find_by_id(subcategory_id) + category_topic_destroyer(s, destroy_system_topics) + end + category_topic_destroyer(c, destroy_system_topics) + end + + def destroy_groups groups = Group.where(automatic: false) - log = [] groups.each do |group| - log << "destroying group: #{group.id}" - log << group.destroy + @io.puts "destroying group: #{group.id}" + @io.puts group.destroy end - log end - def self.destroy_users - log = [] + def destroy_users users = User.where(admin: false, id: 1..Float::INFINITY) - log << "There are #{users.count} users to delete" + @io.puts "There are #{users.count} users to delete" options = {} options[:delete_posts] = true current_user = User.find(-1) #system users.each do |user| begin if UserDestroyer.new(current_user).destroy(user, options) - log << "#{user.username} deleted" + @io.puts "#{user.username} deleted" else - log << "#{user.username} not deleted" + @io.puts "#{user.username} not deleted" end rescue UserDestroyer::PostsExistError raise Discourse::InvalidAccess.new("User #{user.username} has #{user.post_count} posts, so can't be deleted.") rescue NoMethodError - log << "#{user.username} could not be deleted" + @io.puts "#{user.username} could not be deleted" end end - log end - def self.destroy_stats + def destroy_stats ApplicationRequest.destroy_all IncomingLink.destroy_all UserVisit.destroy_all @@ -90,4 +117,13 @@ class DestroyTask PostAction.unscoped.destroy_all EmailLog.destroy_all end + + private + + def category_topic_destroyer(category, destroy_system_topics = false) + destroy_topics_log = destroy_topics_in_category(category.id, destroy_system_topics) + @io.puts "Destroying #{category.slug} category" + category.destroy + end + end diff --git a/lib/tasks/categories.rake b/lib/tasks/categories.rake index 97341f59b8b..b2c9ab2c9f0 100644 --- a/lib/tasks/categories.rake +++ b/lib/tasks/categories.rake @@ -36,3 +36,11 @@ end def print_status(current, max) print "\r%9d / %d (%5.1f%%)" % [current, max, ((current.to_f / max.to_f) * 100).round(1)] end + +desc "Output a list of categories" +task "categories:list" => :environment do + categories = Category.pluck(:id, :slug, :parent_category_id) + categories.each do |c| + puts "id: #{c[0]}, slug: #{c[1]}, parent: #{c[2]}" + end +end diff --git a/lib/tasks/destroy.rake b/lib/tasks/destroy.rake index 767f14370c9..dd99bf52deb 100644 --- a/lib/tasks/destroy.rake +++ b/lib/tasks/destroy.rake @@ -1,43 +1,60 @@ # frozen_string_literal: true ## These tasks are destructive and are for clearing out all the -# content and users from your site, but keeping your site settings, -# theme, and category structure. +# content and users from your site. desc "Remove all topics in a category" task "destroy:topics", [:category, :parent_category] => :environment do |t, args| + destroy_task = DestroyTask.new category = args[:category] parent_category = args[:parent_category] descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category puts "Going to delete all topics in the #{descriptive_slug} category" - puts log = DestroyTask.destroy_topics(category, parent_category) + destroy_task.destroy_topics(category, parent_category) end desc "Remove all topics in all categories" task "destroy:topics_all_categories" => :environment do + destroy_task = DestroyTask.new puts "Going to delete all topics in all categories..." - puts log = DestroyTask.destroy_topics_all_categories + puts log = destroy_task.destroy_topics_all_categories end desc "Remove all private messages" task "destroy:private_messages" => :environment do + destroy_task = DestroyTask.new puts "Going to delete all private messages..." - puts log = DestroyTask.destroy_private_messages + puts log = destroy_task.destroy_private_messages end desc "Destroy all groups" task "destroy:groups" => :environment do + destroy_task = DestroyTask.new puts "Going to delete all non-default groups..." - puts log = DestroyTask.destroy_groups + puts log = destroy_task.destroy_groups end desc "Destroy all non-admin users" task "destroy:users" => :environment do + destroy_task = DestroyTask.new puts "Going to delete all non-admin users..." - puts log = DestroyTask.destroy_users + puts log = destroy_task.destroy_users end desc "Destroy site stats" task "destroy:stats" => :environment do + destroy_task = DestroyTask.new puts "Going to delete all site stats..." - DestroyTask.destroy_stats + destroy_task.destroy_stats +end + +# Example: rake destroy:categories[28,29,44,85] +# Run rake categories:list for a list of category ids +desc "Destroy a comma separated list of category ids." +task "destroy:categories" => :environment do |t, args| + destroy_task = DestroyTask.new + categories = args.extras + puts "Going to delete these categories: #{categories}" + categories.each do |id| + destroy_task.destroy_category(id, true) + end end diff --git a/spec/services/destroy_task_spec.rb b/spec/services/destroy_task_spec.rb index e33276710b1..9d81aa2c46f 100644 --- a/spec/services/destroy_task_spec.rb +++ b/spec/services/destroy_task_spec.rb @@ -11,37 +11,68 @@ describe DestroyTask do fab!(:c2) { Fabricate(:category) } fab!(:t2) { Fabricate(:topic, category: c2) } let!(:p2) { Fabricate(:post, topic: t2) } - fab!(:sc) { Fabricate(:category, parent_category: c) } + fab!(:sc) { Fabricate(:category, parent_category: c2) } fab!(:t3) { Fabricate(:topic, category: sc) } let!(:p3) { Fabricate(:post, topic: t3) } it 'destroys all topics in a category' do - expect { DestroyTask.destroy_topics(c.slug) } + destroy_task = DestroyTask.new(StringIO.new) + expect { destroy_task.destroy_topics(c.slug) } .to change { Topic.where(category_id: c.id).count }.by (-1) end it 'destroys all topics in a sub category' do - expect { DestroyTask.destroy_topics(sc.slug, c.slug) } + destroy_task = DestroyTask.new(StringIO.new) + expect { destroy_task.destroy_topics(sc.slug, c2.slug) } .to change { Topic.where(category_id: sc.id).count }.by(-1) end it "doesn't destroy system topics" do - DestroyTask.destroy_topics(c2.slug) + destroy_task = DestroyTask.new(StringIO.new) + destroy_task.destroy_topics(c2.slug) expect(Topic.where(category_id: c2.id).count).to eq 1 end it 'destroys topics in all categories' do - DestroyTask.destroy_topics_all_categories + destroy_task = DestroyTask.new(StringIO.new) + destroy_task.destroy_topics_all_categories expect(Post.where(topic_id: [t.id, t2.id, t3.id]).count).to eq 0 end end + describe 'destroy categories' do + fab!(:c) { Fabricate(:category) } + fab!(:t) { Fabricate(:topic, category: c) } + let!(:p) { Fabricate(:post, topic: t) } + fab!(:c2) { Fabricate(:category) } + fab!(:t2) { Fabricate(:topic, category: c) } + let!(:p2) { Fabricate(:post, topic: t2) } + fab!(:sc) { Fabricate(:category, parent_category: c2) } + fab!(:t3) { Fabricate(:topic, category: sc) } + let!(:p3) { Fabricate(:post, topic: t3) } + + it 'destroys specified category' do + destroy_task = DestroyTask.new(StringIO.new) + + expect { destroy_task.destroy_category(c.id) } + .to change { Category.where(id: c.id).count }.by (-1) + end + + it 'destroys sub-categories when destroying parent category' do + destroy_task = DestroyTask.new(StringIO.new) + + expect { destroy_task.destroy_category(c2.id) } + .to change { Category.where(id: sc.id).count }.by (-1) + end + end + describe 'private messages' do let!(:pm) { Fabricate(:private_message_post) } let!(:pm2) { Fabricate(:private_message_post) } it 'destroys all private messages' do - DestroyTask.destroy_private_messages + destroy_task = DestroyTask.new(StringIO.new) + destroy_task.destroy_private_messages expect(Topic.where(archetype: "private_message").count).to eq 0 end end @@ -51,13 +82,15 @@ describe DestroyTask do let!(:g2) { Fabricate(:group) } it 'destroys all groups' do - DestroyTask.destroy_groups + destroy_task = DestroyTask.new(StringIO.new) + destroy_task.destroy_groups expect(Group.where(automatic: false).count).to eq 0 end it "doesn't destroy default groups" do + destroy_task = DestroyTask.new(StringIO.new) before_count = Group.count - DestroyTask.destroy_groups + destroy_task.destroy_groups expect(Group.count).to eq before_count - 2 end end @@ -70,7 +103,8 @@ describe DestroyTask do Fabricate(:user) Fabricate(:admin) - DestroyTask.destroy_users + destroy_task = DestroyTask.new(StringIO.new) + destroy_task.destroy_users expect(User.where(admin: false).count).to eq 0 # admin does not get detroyed expect(User.count).to eq before_count + 1 @@ -79,7 +113,8 @@ describe DestroyTask do describe 'stats' do it 'destroys all site stats' do - DestroyTask.destroy_stats + destroy_task = DestroyTask.new(StringIO.new) + destroy_task.destroy_stats end end end