FEATURE: Create a rake task for destroying categories

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.

diff --git a/app/services/destroy_task.rb b/app/services/destroy_task.rb
index f7c8a64..e5ff8e5 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
+  end
+
+  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
-    log
+    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 self.destroy_topics_all_categories
+  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
+  end
+
+  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
-    log
+    category_topic_destroyer(c, destroy_system_topics)
   end
 
-  def self.destroy_groups
+  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 97341f5..b2c9ab2 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 767f143..dd99bf5 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

[... diff too long, it was truncated ...]

GitHub sha: 092eeb5c

2 Likes

This should probably be sorted. Maybe also display all the children under their parents?

1 Like

Since there could be a LOT of topics, it’s better to use find_each instead of each in order to only load 1000s topics in memory when doing the loop.

2 Likes

It’s better to user Discourse.system_user since it’s only loaded once and then cached :wink:

1 Like

Discourse.system_user is your best friend

1 Like

Since there might be a LOT of PMs, find_each is a better fit there as well.

1 Like

Since it’s only the ids, subcategory_ids would be a better name. (Or subcategories_ids I never know which one is best)

1 Like

Also, not sure why you’re not directly loading all the objects here since you’re doing Category.find_by_id later in the loop?

1 Like

Unless we’ve defined the helper in Discourse, all the find_by_<something>(<value>) helpers are deprecated and we should instead use find_by(something: <value>)

1 Like

Discourse.system_user

1 Like

That id: 1..Float::INFINITY is weird. If you want to exclude all the robots, you can use the human_users scope like this

User.human_users.where(admin: false).find_each do |user|
  # do stuff
end
1 Like

Is destroy_topics_log being used anywhere?

1 Like

Why remove the puts here?

Since you’re not doing anything with the destroy_task and log variable, you can just do something like this

task "destroy:topics_all_categories" => :environment do
  puts "Going to delete all topics in all categories..."
  puts DestroyTask.new.destroy_topics_all_categories
end

end

1 Like

Because it just outputs an empty string

1 Like

REFACTOR: Cleanup rake tasks based on feedback