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 # 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 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) c = Category.find_by_slug(category, parent_category)
log = []
descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category 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?
topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1) if delete_system_topics
log << "There are #{topics.count} topics to delete in #{descriptive_slug} category" 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| topics.each do |topic|
log << "Deleting #{topic.slug}..." @io.puts "Deleting #{topic.slug}..."
first_post = topic.ordered_posts.first first_post = topic.ordered_posts.first
if first_post.nil? if first_post.nil?
return log << "Topic.ordered_posts.first was nil" return @io.puts "Topic.ordered_posts.first was nil"
end end
system_user = User.find(-1) system_user = User.find(-1)
log << PostDestroyer.new(system_user, first_post).destroy @io.puts PostDestroyer.new(system_user, first_post).destroy
end end
log
end 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 categories = Category.all
log = []
categories.each do |c| categories.each do |c|
log << destroy_topics(c.slug, c.parent_category&.slug) @io.puts destroy_topics(c.slug, c.parent_category&.slug)
end end
log
end end
def self.destroy_private_messages def destroy_private_messages
pms = Topic.where(archetype: "private_message") pms = Topic.where(archetype: "private_message")
current_user = User.find(-1) #system current_user = User.find(-1) #system
log = []
pms.each do |pm| pms.each do |pm|
log << "Destroying #{pm.slug} pm" @io.puts "Destroying #{pm.slug} pm"
first_post = pm.ordered_posts.first 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
log
end 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) groups = Group.where(automatic: false)
log = []
groups.each do |group| groups.each do |group|
log << "destroying group: #{group.id}" @io.puts "destroying group: #{group.id}"
log << group.destroy @io.puts group.destroy
end end
log
end end
def self.destroy_users def destroy_users
log = []
users = User.where(admin: false, id: 1..Float::INFINITY) 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 = {}
options[:delete_posts] = true options[:delete_posts] = true
current_user = User.find(-1) #system current_user = User.find(-1) #system
users.each do |user| users.each do |user|
begin begin
if UserDestroyer.new(current_user).destroy(user, options) if UserDestroyer.new(current_user).destroy(user, options)
log << "#{user.username} deleted" @io.puts "#{user.username} deleted"
else else
log << "#{user.username} not deleted" @io.puts "#{user.username} not deleted"
end end
rescue UserDestroyer::PostsExistError rescue UserDestroyer::PostsExistError
raise Discourse::InvalidAccess.new("User #{user.username} has #{user.post_count} posts, so can't be deleted.") raise Discourse::InvalidAccess.new("User #{user.username} has #{user.post_count} posts, so can't be deleted.")
rescue NoMethodError rescue NoMethodError
log << "#{user.username} could not be deleted" @io.puts "#{user.username} could not be deleted"
end end
end end
log
end end
def self.destroy_stats def destroy_stats
ApplicationRequest.destroy_all ApplicationRequest.destroy_all
IncomingLink.destroy_all IncomingLink.destroy_all
UserVisit.destroy_all UserVisit.destroy_all
@ -90,4 +117,13 @@ class DestroyTask
PostAction.unscoped.destroy_all PostAction.unscoped.destroy_all
EmailLog.destroy_all EmailLog.destroy_all
end 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 end

View File

@ -36,3 +36,11 @@ end
def print_status(current, max) def print_status(current, max)
print "\r%9d / %d (%5.1f%%)" % [current, max, ((current.to_f / max.to_f) * 100).round(1)] print "\r%9d / %d (%5.1f%%)" % [current, max, ((current.to_f / max.to_f) * 100).round(1)]
end 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 # frozen_string_literal: true
## These tasks are destructive and are for clearing out all the ## These tasks are destructive and are for clearing out all the
# content and users from your site, but keeping your site settings, # content and users from your site.
# theme, and category structure.
desc "Remove all topics in a category" desc "Remove all topics in a category"
task "destroy:topics", [:category, :parent_category] => :environment do |t, args| task "destroy:topics", [:category, :parent_category] => :environment do |t, args|
destroy_task = DestroyTask.new
category = args[:category] category = args[:category]
parent_category = args[:parent_category] parent_category = args[:parent_category]
descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category
puts "Going to delete all topics in the #{descriptive_slug} 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 end
desc "Remove all topics in all categories" desc "Remove all topics in all categories"
task "destroy:topics_all_categories" => :environment do task "destroy:topics_all_categories" => :environment do
destroy_task = DestroyTask.new
puts "Going to delete all topics in all categories..." 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 end
desc "Remove all private messages" desc "Remove all private messages"
task "destroy:private_messages" => :environment do task "destroy:private_messages" => :environment do
destroy_task = DestroyTask.new
puts "Going to delete all private messages..." puts "Going to delete all private messages..."
puts log = DestroyTask.destroy_private_messages puts log = destroy_task.destroy_private_messages
end end
desc "Destroy all groups" desc "Destroy all groups"
task "destroy:groups" => :environment do task "destroy:groups" => :environment do
destroy_task = DestroyTask.new
puts "Going to delete all non-default groups..." puts "Going to delete all non-default groups..."
puts log = DestroyTask.destroy_groups puts log = destroy_task.destroy_groups
end end
desc "Destroy all non-admin users" desc "Destroy all non-admin users"
task "destroy:users" => :environment do task "destroy:users" => :environment do
destroy_task = DestroyTask.new
puts "Going to delete all non-admin users..." puts "Going to delete all non-admin users..."
puts log = DestroyTask.destroy_users puts log = destroy_task.destroy_users
end end
desc "Destroy site stats" desc "Destroy site stats"
task "destroy:stats" => :environment do task "destroy:stats" => :environment do
destroy_task = DestroyTask.new
puts "Going to delete all site stats..." 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 end

View File

@ -11,37 +11,68 @@ describe DestroyTask do
fab!(:c2) { Fabricate(:category) } fab!(:c2) { Fabricate(:category) }
fab!(:t2) { Fabricate(:topic, category: c2) } fab!(:t2) { Fabricate(:topic, category: c2) }
let!(:p2) { Fabricate(:post, topic: t2) } 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) } fab!(:t3) { Fabricate(:topic, category: sc) }
let!(:p3) { Fabricate(:post, topic: t3) } let!(:p3) { Fabricate(:post, topic: t3) }
it 'destroys all topics in a category' do 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) .to change { Topic.where(category_id: c.id).count }.by (-1)
end end
it 'destroys all topics in a sub category' do 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) .to change { Topic.where(category_id: sc.id).count }.by(-1)
end end
it "doesn't destroy system topics" do 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 expect(Topic.where(category_id: c2.id).count).to eq 1
end end
it 'destroys topics in all categories' do 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 expect(Post.where(topic_id: [t.id, t2.id, t3.id]).count).to eq 0
end end
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 describe 'private messages' do
let!(:pm) { Fabricate(:private_message_post) } let!(:pm) { Fabricate(:private_message_post) }
let!(:pm2) { Fabricate(:private_message_post) } let!(:pm2) { Fabricate(:private_message_post) }
it 'destroys all private messages' do 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 expect(Topic.where(archetype: "private_message").count).to eq 0
end end
end end
@ -51,13 +82,15 @@ describe DestroyTask do
let!(:g2) { Fabricate(:group) } let!(:g2) { Fabricate(:group) }
it 'destroys all groups' do 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 expect(Group.where(automatic: false).count).to eq 0
end end
it "doesn't destroy default groups" do it "doesn't destroy default groups" do
destroy_task = DestroyTask.new(StringIO.new)
before_count = Group.count before_count = Group.count
DestroyTask.destroy_groups destroy_task.destroy_groups
expect(Group.count).to eq before_count - 2 expect(Group.count).to eq before_count - 2
end end
end end
@ -70,7 +103,8 @@ describe DestroyTask do
Fabricate(:user) Fabricate(:user)
Fabricate(:admin) Fabricate(:admin)
DestroyTask.destroy_users destroy_task = DestroyTask.new(StringIO.new)
destroy_task.destroy_users
expect(User.where(admin: false).count).to eq 0 expect(User.where(admin: false).count).to eq 0
# admin does not get detroyed # admin does not get detroyed
expect(User.count).to eq before_count + 1 expect(User.count).to eq before_count + 1
@ -79,7 +113,8 @@ describe DestroyTask do
describe 'stats' do describe 'stats' do
it 'destroys all site stats' do it 'destroys all site stats' do
DestroyTask.destroy_stats destroy_task = DestroyTask.new(StringIO.new)
destroy_task.destroy_stats
end end
end end
end end