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.
This commit is contained in:
Blake Erickson 2019-07-17 12:41:13 -06:00
parent 98866ca043
commit 092eeb5ca3
4 changed files with 148 additions and 52 deletions

View File

@ -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?
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)
log << "There are #{topics.count} topics to delete in #{descriptive_slug} category"
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

View File

@ -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

View File

@ -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

View File

@ -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