mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:39:43 +08:00
Previously, `activity_by_category` aggregated `topics`, `posts` and `topic_view_stats` across the whole reporting window on every dashboard request, and because the category filter was only applied after the join, selecting three categories cost the same as scanning all of them. On a 600k topic / 4.8M post dataset that was ~440ms per request, per admin, growing with the site. This change persists per-day, per-category counts in `category_activity_daily_rollups` and has the report read only those, cutting the same query to ~4ms. `Jobs::MaintainCategoryActivityDailyRollups` refreshes recent days every three hours, while `Jobs::RebuildCategoryActivityDailyRollups` recalculates history daily so topics that are recategorised or deleted after the fact are reattributed rather than left frozen. Invalidating on change was rejected because `PostMover`, bulk category actions, imports and rake tasks all rewrite historical attribution without firing an event.
180 lines
6.2 KiB
Ruby
Vendored
180 lines
6.2 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require "highline"
|
|
require "io/console"
|
|
|
|
class DestroyTask
|
|
def initialize(io = STDOUT)
|
|
@io = io
|
|
@tty = IO.console
|
|
@hl = HighLine.new(@tty, @io)
|
|
end
|
|
|
|
def destroy_topics(category, parent_category = nil, delete_system_topics = false)
|
|
c = Category.find_by_slug(category, parent_category)
|
|
descriptive_slug = parent_category ? "#{parent_category}/#{category}" : 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.find_each do |topic|
|
|
@io.puts "Deleting #{topic.slug}..."
|
|
first_post = topic.ordered_posts.first
|
|
return @io.puts "Topic.ordered_posts.first was nil" if first_post.nil?
|
|
@io.puts PostDestroyer.new(
|
|
Discourse.system_user,
|
|
first_post,
|
|
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
|
|
).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.find_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?
|
|
PostDestroyer.new(
|
|
Discourse.system_user,
|
|
first_post,
|
|
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
|
|
).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.each { |c| @io.puts destroy_topics(c.slug, c.parent_category&.slug) }
|
|
end
|
|
|
|
def destroy_posts(post_ids, require_confirmation: true)
|
|
if !SiteSetting.can_permanently_delete
|
|
@io.puts "The can_permanently_delete site setting needs to be enabled to destroy posts."
|
|
return
|
|
end
|
|
|
|
posts = Post.with_deleted.where(id: post_ids)
|
|
|
|
@io.puts(<<~NOTICE)
|
|
There are #{posts.count} posts to delete.
|
|
|
|
Note: If a post is the first post in a topic, all posts
|
|
in that topic will be deleted.
|
|
NOTICE
|
|
|
|
if posts.count < post_ids.size
|
|
@io.puts "Couldn't find the following posts:"
|
|
@io.puts " #{post_ids.map(&:to_i) - posts.pluck(:id)}"
|
|
end
|
|
|
|
if require_confirmation
|
|
confirm_destroy = @hl.ask("Are you sure? (Y/n)")
|
|
exit 1 if confirm_destroy.downcase != "y"
|
|
end
|
|
|
|
# Do first posts before the rest. The PostDestroyer will destroy
|
|
# all posts following a first post automatically, and we want to
|
|
# avoid duplicate audit trail entries by trying to delete them
|
|
# again. Note: Rails batching will order by pkey (post ID in this
|
|
# case) internally, and can not accept additional ordering options.
|
|
[posts.where(post_number: 1), posts].each do |scope|
|
|
scope.find_each(order: :desc) do |post|
|
|
@io.puts "Destroying post #{post.id}"
|
|
@io.puts PostDestroyer.new(
|
|
Discourse.system_user,
|
|
post,
|
|
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
|
|
force_destroy: true,
|
|
).destroy
|
|
end
|
|
end
|
|
end
|
|
|
|
def destroy_private_messages
|
|
Topic
|
|
.where(archetype: "private_message")
|
|
.find_each do |pm|
|
|
@io.puts "Destroying #{pm.slug} pm"
|
|
first_post = pm.ordered_posts.first
|
|
@io.puts PostDestroyer.new(
|
|
Discourse.system_user,
|
|
first_post,
|
|
context: I18n.t("staff_action_logs.cli_bulk_post_delete"),
|
|
).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)
|
|
@io.puts "There are #{subcategories.count} subcategories to delete" if subcategories
|
|
subcategories.each { |s| category_topic_destroyer(s, destroy_system_topics) }
|
|
category_topic_destroyer(c, destroy_system_topics)
|
|
end
|
|
|
|
def destroy_groups
|
|
groups = Group.where(automatic: false)
|
|
groups.each do |group|
|
|
@io.puts "destroying group: #{group.id}"
|
|
@io.puts group.destroy
|
|
end
|
|
end
|
|
|
|
def destroy_users
|
|
User
|
|
.human_users
|
|
.where(admin: false)
|
|
.find_each do |user|
|
|
if UserDestroyer.new(Discourse.system_user).destroy(
|
|
user,
|
|
delete_posts: true,
|
|
context: "destroy task",
|
|
)
|
|
@io.puts "#{user.username} deleted"
|
|
else
|
|
@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
|
|
@io.puts "#{user.username} could not be deleted"
|
|
rescue Discourse::InvalidAccess => e
|
|
@io.puts "#{user.username} #{e.message}"
|
|
end
|
|
end
|
|
|
|
def destroy_stats
|
|
ApplicationRequest.delete_all
|
|
IncomingLink.delete_all
|
|
UserVisit.delete_all
|
|
UserVisitDailyRollup.delete_all
|
|
CategoryActivityDailyRollup.delete_all
|
|
UserProfileView.delete_all
|
|
UserProfile.update_all(views: 0)
|
|
PostAction.unscoped.delete_all
|
|
EmailLog.delete_all
|
|
end
|
|
|
|
private
|
|
|
|
def category_topic_destroyer(category, destroy_system_topics = false)
|
|
destroy_topics_in_category(category.id, destroy_system_topics)
|
|
@io.puts "Destroying #{category.slug} category"
|
|
category.destroy
|
|
end
|
|
end
|