Operaciones masivas administrativas

Below you will find a collection of bulk operations that can be initiated from the command line. You will need SSH access, so if you are a hosted customer, you will need to contact the Discourse team about running these commands.

:warning: Before working with the console it is extremely important that you have a recent backup. Mistakes can always happen!

First thing to do is enter your site’s container:

cd /var/discourse
./launcher enter app

Additional Guides:

Change topic status


Before running the following commands, run rails c to enter the console.

  • Unlist all topics within a category (excludes post action)

    You can replace visible with closed or archived and adjust the true/false values as needed

    cat_id = Category.find_by_slug('admins').id
    Topic.where(category_id: cat_id, visible: true).update_all(visible: false)
    
  • Unlist all topics within a category (includes post action)

    cat_id = Category.find_by_slug("admins").id
    Topic.where(category_id: cat_id, visible: true).find_each do |topic| 
      topic.update_status('visible', false, Discourse.system_user)
    end
    
  • Close all topics created before a specified date (includes post action)

    Topic.where(closed: false).where("created_at < '2015-01-01'").find_each do |topic| 
      topic.update_status('closed', true, Discourse.system_user)
    end
    

Moving topics


Move a collection of topics from one category to another

rails c
topic_ids = [12,16,29]
cat_to = Category.find_by_slug('faq')
Topic.where(id: topic_ids).update_all(category_id: cat_to.id)
Category.update_stats

Users

Delete subset of users


Delete users that have never posted and have not visited since a specified date

rails c
User.joins(:user_stat).where("user_stats.post_count = 0 AND previous_visit_at <= '2016-05-20'::timestamp").destroy_all

Suspend a set of users based on criteria


Set who will be logged as suspending the users

rails c
logger = StaffActionLogger.new(User.find_by(username_lower: "tshenry"))

Create a suspension timeframe and reason

suspend_till = DateTime.new(2057,12,31)
reason = 'Completed Course'

In this example, our user criteria will be group membership.

target_group = Group.find_by_name("summer_students")
users = User.joins(:group_users).where(group_users: {group_id: target_group.id})

Suspend each user based on the values established above:

users.find_each do |u|
  u.suspended_till = suspend_till
  u.suspended_at = DateTime.now
  u.save!

  logger.log_user_suspend(u,reason)
  putc '.'
end

Update user suspension reasons


Perhaps you suspended users who completed a class (see example above), and now you want to add the year of the class as you’ve taught multiple years.

UserHistory.where(action: 10, details: "Completed Course").update_all(details: "Completed 2018 Course")

Unsuspend users


If you need to unsuspend users in bulk, say because they were part of a previous year’s cohort and are returning for this year, you can do so as shown below. In the example, we’re finding users by their user id.

user_list = [1, 3, 5, 7, 11]
users = User.where("id in (?)", user_list)

users.each do |user|
    user.suspended_till = nil
    user.suspended_at = nil
    user.save!
    StaffActionLogger.new(User.find(-1)).log_user_unsuspend(user)

    DiscourseEvent.trigger(:user_unsuspended, user: user)
end

Export/Import

Export/Import all site settings


To simply print out all of the settings that have been changed on your site, run:

rake site_settings:export

If you want to export the settings to a file:

rake site_settings:export > saved_settings.yml

If you want to import settings from a file:

rake site_settings:import < saved_settings.yml

Export/Import categories


There are two options for exporting and one method to handle importing.

Export a set of complete categories

First get a list of your category IDs:

rake categories:list

Then space-separate the category IDs in the export rake task. For example:

rake export:categories["12 6"]

Export your site’s category structure

This is essentially copying the “skeleton” of your Discourse site. It includes every category along with any groups associated with existing category permissions. It does not include topics:

rake export:category_structure

If you want the category structure along with any groups associated with the category permissions and any members of those groups:

rake export:category_structure[true]

Importing a category file

Use the exported file’s name like the example below:

rake import:file["category-export-2019-05-16-052430.json"]

Export/Import groups


Export all of the user groups

rake export:groups

Export all of the user groups including users

rake export:groups[true]

Importing a group file
Use the exported file’s name like the example below:

rake import:file["group-export-2019-05-16-052430.json"]

Set permissions for multiple categories


:warning: Note that this will remove any existing access restrictions you have set up for the categories involved. Make sure to include all of the relevant permissions.

  1. Get a list of categories along with their IDs

    rails c
    Category.all.pluck("name", "id")
    
  2. Create an array with the category IDs you wish to target.

    category_ids = [6,7,8,10]
    
  3. Change the permissions. The set_permissions function can utilize the following parameters: :full, :create_post, :readonly

    • A single permission. For example, making a set of categories staff only:

      Category.where(id: category_ids).find_each do |category| 
       category.set_permissions(:staff => :full)
       category.save!
      end
      
    • Multiple permissions. For example, making a set of categories read-only for normal users:

      Category.where(id: category_ids).find_each do |category| 
        category.set_permissions(:everyone => :readonly, :staff => :full)
        category.save!
      end
      
    • User group permissions. For example giving one group full permissions and another group read-only for a set of categories:

      artists_group = Group.find_by_name("artists")
      buyers_group = Group.find_by_name("buyers")
      Category.where(id: category_ids).find_each do |category| 
        category.set_permissions(artists_group.id => :full, buyers_group.id => :readonly)
        category.save!
      end
      

Bulk Tag All Topics Based on a Keyword

The following script will allow you to tag topics based on the presence of a keyword in the topic title or its posts. Start by creating an array of keywords:

rails c
keywords = ['apples','oranges']

Next we need to define a method:

def tag_by_keyword(word, tag_name)
  tag = Tag.find_by_name(tag_name) || Tag.create(name: tag_name)
  keyword_topics = Topic.joins(:posts).where("topics.title ~* :keyword or posts.raw ~* :keyword", keyword: "\\y#{word}\\y").distinct

  keyword_topics.each do |topic|
    if topic.tags.exclude?(tag)
      topic.tags << tag
    end
  end
end

And finally run each keyword through the method. The following with tag each relevant topic with a tag called “fruit”:

keywords.each { |word| tag_by_keyword(word, 'fruit') }

Bulk Tag All Topics Within a Category


Template: rake tags:bulk_tag_category["<tag>|<tag>",<category_id>]
This would be particularly useful when trying to convert a category to a tag.

First, use the following rake task to find the relevant category ID.

rake categories:list

Tag all topics of the category you specify. In this example, you would be tagging all topics in the category with an ID of 6 with the “support” tag. :warning: this will remove all other tags from each topic.

rake tags:bulk_tag_category["support",6]

Append all topics of the category you specify. In this example, you would be adding the “support” tag to all topics in the category with an ID of 6, while keeping existing tags.

rake tags:bulk_tag_category["support",6,true]

Move all topics with a specific tag to a single category

When trying to restructure your Discourse site, you may find that you want to move a collection of topics without triggering any notifications. One way to do this is to create a temporary tag, apply the tag to appropriate topics, move the topics to a specific category using the code below, then finally delete the temporary tag.

Get the tag.

rails c
tag = Tag.find_by_name("tutorial")

Get destination category.

  • For regular categories:
cat_to = Category.find_by_slug('guides')
  • For subcategories:
cat_to = Category.find_by_slug('child-slug','parent-slug')

Move the tagged topics to the destination category.

Topic.joins(:topic_tags).where("topic_tags.tag_id = ?", tag.id).update_all(category_id: cat_to.id)

Update the topic counts of the affected categories.

Category.update_stats
CategoryTagStat.update_topic_counts

Move all topics from one category to another


Find the category IDs with the following rake task:

rake categories:list

The first value should be the starting category ID. The second value should be the destination category ID.

rake categories:move_topics[15,6]
Rails Console Script
cat_from_id = XX # Category to move topics from 
cat_to_id = XX  # Category to move topics to 
Topic.where(category_id: cat_from_id).update_all(category_id: cat_to_id)
Category.update_stats
CategoryTagStat.update_topic_counts

Change owner of all topics in categories


Find the category IDs with the following rake task:

rake categories:list

Specify the new owner and categories to operate on. The categories should be an array of category ids, categories 1, 2 and 3 in the example:

rails c
user = User.find_by(username_lower: "lowercase-username")
categories = [1, 2, 3]

Get all topic ids for the given categories and change the owner of the first post in all matched topics.

topics = Topic.where(category_id: categories).pluck(:id)

topics.each do |topic|
  PostOwnerChanger.new(
    post_ids: Post.where(topic_id: topic).where(post_number: 1).pluck(:id),
    topic_id: topic,
    new_owner: user,
    acting_user: Discourse.system_user,
    skip_revision: true
  ).change_owner!
end

Grant a badge to all group members


Grant a badge to all users that belong to a specific group. The first value is the group ID and the second is the badge ID.

rails c
Group.find_by_name("event_participants").id
Badge.find_by_name("event_badge").id
exit
rake groups:grant_badge[42,102]

:warning: Note that the above rake task only grants a badge, it will not revoke a previously granted badge if a user is no longer part of the specified group. If you need to bulk revoke badges for all users that are no longer part of a group, you can run the following:

rails c

badge_id = Badge.find_by_name("Some Group Member").id

group = Group.find_by_name("Some_Group")

group_user_id = group.users.pluck("id")

userBadge = UserBadge.where.not(user_id: group_user_id).where(badge_id: badge_id)

userBadge.each do |ub|
  BadgeGranter.revoke(ub, revoked_by: Discourse.system_user)
end

exit

Ensure all users are at their automatic trust level


Say you set the default trust level for new or invited users to a value that isn’t working out quite the way you expected (such as TL4). Now you want to change it so your users are at the trust level they would be automatically, given their current stats. The following commands will ensure all users are at the trust level they should be according to Understanding Discourse Trust Levels. Note: users with locked trust-levels will not be affected.

Make sure all users are set to the correct trust level:

rails c
User.all.find_each do |user|
  Promotion.recalculate(user)
end

Refresh the group stats to reflect the changes:

Group.ensure_consistency!

Topic maintenance scripts

The following ruby scripts demonstrate how to perform automated maintenance on topics based on activity dates and other criteria. These scripts combine SQL queries to identify topics with Ruby code to perform actions on them, and must be run via the rails console for your site.

Each script follows a similar pattern:

  1. A SQL query that identifies relevant topics
  2. Ruby code that processes each topic and applies the desired actions
  3. Basic error handling and logging

These scripts can be customized by:

  • Adjusting time periods (e.g., ‘6 MONTH’, ‘1 YEAR’, ‘2 YEAR’)
  • Changing category selections to match your forum structure
  • Modifying which actions to take (close, unlist, or move)
  • Adding additional conditions like post count or view thresholds

Close, Unlist, and Move Inactive Topics

This script identifies topics that meet the following criteria:

  • In a specific category
  • Open
  • Unsolved (using the Discourse Solved Plugin)
  • No recent activity within a specific timeframe

Then performs multiple actions:

  • Closes them,
  • Unlists them, and
  • Moves them to a designated category for outdated content
SQL Query
WITH topic_list AS (
    SELECT ua.target_topic_id, MAX(ua.created_at) "created_at"  
    FROM user_actions ua
    INNER JOIN topics t ON t.id = ua.target_topic_id
    INNER JOIN categories c ON c.id = t.category_id
    LEFT JOIN discourse_solved_solved_topics solved ON solved.topic_id = t.id
    WHERE t.closed = false
        AND t.category_id = [CATEGORY_ID]
        AND solved.topic_id IS NULL
        AND t.deleted_at IS NULL
    GROUP BY ua.target_topic_id
    HAVING MAX(ua.created_at) <= (CURRENT_DATE - (INTERVAL '[TIME_PERIOD]'))
    ORDER BY "created_at" DESC
)
    
SELECT '' AS total, target_topic_id AS topic_id, created_at 
FROM topic_list
UNION
SELECT ''||COUNT(*), 0, CURRENT_DATE
FROM topic_list
ORDER BY created_at DESC
Combined SQL + Script
sql = "WITH topic_list AS (
    SELECT ua.target_topic_id, MAX(ua.created_at) \"created_at\"  
    FROM user_actions ua
    INNER JOIN topics t ON t.id = ua.target_topic_id
    INNER JOIN categories c ON c.id = t.category_id
    LEFT JOIN discourse_solved_solved_topics solved ON solved.topic_id = t.id
    WHERE t.closed = false
        AND t.category_id = [CATEGORY_ID]
        AND solved.topic_id IS NULL
        AND t.deleted_at IS NULL
    GROUP BY ua.target_topic_id
    HAVING MAX(ua.created_at) <= (CURRENT_DATE - (INTERVAL '[TIME_PERIOD]'))
    ORDER BY \"created_at\" DESC
)
    
SELECT '' AS total, target_topic_id AS topic_id, created_at 
FROM topic_list
UNION
SELECT ''||COUNT(*), 0, CURRENT_DATE
FROM topic_list
ORDER BY created_at DESC"

results = ActiveRecord::Base.connection.execute(sql)
user = Discourse.system_user
destination_category = Category.find([DESTINATION_CATEGORY_ID])

puts "Found #{results.count} topics to process"

results.each do |row|
    begin
    topic = Topic.find(row["topic_id"])
    
    # 1. Move to destination category
    topic.update!(category_id: destination_category.id)
    puts "#{topic.id} moved to destination category"
    
    # 2. Close the topic
    topic.update_status('closed', true, user, until: nil)
    puts "#{topic.id} is closed"
    
    # 3. Unlist the topic
    topic.update_status('visible', false, user, until: nil)
    puts "#{topic.id} is unlisted"

    # Error Handling 
    rescue => e
      puts "Error processing topic #{row["topic_id"]}: #{e.message}"
    end
end

puts "Process completed"

Close Solved Topics with No Recent Activity

This script closes solved topics that have been inactive for a defined period. This can help keep your forum tidy while preserving valuable solved topics.

This script identifies topics that meet the following criteria:

  • In a specific category
  • Open
  • Solved (using the Discourse Solved Plugin)
  • No recent activity within a specific timeframe
SQL Query
WITH topic_list AS (
    SELECT ua.target_topic_id, MAX(ua.created_at) "created_at"  
    FROM user_actions ua
    INNER JOIN topics t ON t.id = ua.target_topic_id
    INNER JOIN categories c ON c.id = t.category_id
    INNER JOIN discourse_solved_solved_topics solved ON solved.topic_id = t.id
    WHERE t.closed = false
        AND t.category_id IN ([CATEGORY_IDS])
        AND t.deleted_at IS NULL
    GROUP BY ua.target_topic_id
    HAVING MAX(ua.created_at) <= (CURRENT_DATE - (INTERVAL '[TIME_PERIOD]'))
    ORDER BY "created_at" DESC
)
    
SELECT '' AS total, target_topic_id AS topic_id, created_at 
FROM topic_list
UNION
SELECT ''||COUNT(*), 0, CURRENT_DATE
FROM topic_list
ORDER BY created_at DESC
Combined SQL + Script
sql = "WITH topic_list AS (
    SELECT ua.target_topic_id, MAX(ua.created_at) \"created_at\"  
    FROM user_actions ua
    INNER JOIN topics t ON t.id = ua.target_topic_id
    INNER JOIN categories c ON c.id = t.category_id
    INNER JOIN discourse_solved_solved_topics solved ON solved.topic_id = t.id
    WHERE t.closed = false
        AND t.category_id IN ([CATEGORY_IDS])
        AND t.deleted_at IS NULL
    GROUP BY ua.target_topic_id
    HAVING MAX(ua.created_at) <= (CURRENT_DATE - (INTERVAL '[TIME_PERIOD]'))
    ORDER BY \"created_at\" DESC
)
    
SELECT '' AS total, target_topic_id AS topic_id, created_at 
FROM topic_list
UNION
SELECT ''||COUNT(*), 0, CURRENT_DATE
FROM topic_list
ORDER BY created_at DESC"

results = ActiveRecord::Base.connection.execute(sql)
user = Discourse.system_user

puts "Found #{results.count} topics to process"

results.each do |row|
    begin
    topic = Topic.find(row["topic_id"])
     
    # Close the topic
    topic.update_status('closed', true, user, until: nil)
    puts "#{topic.id} is closed"

    # Error Handling 
    rescue => e
      puts "Error processing topic #{row["topic_id"]}: #{e.message}"
    end
end

puts "Process completed"

Archive Previously Closed Topics

This script identifies topics that were previously closed before a specific date and moves them to an archive category while unlisting them.

SQL Query
WITH topic_list AS (
    SELECT 
        t.id AS topic_id, 
        tt.execute_at AS closed_at
    FROM topics t
    INNER JOIN categories c ON c.id = t.category_id
    LEFT JOIN topic_timers tt ON tt.topic_id = t.id AND tt.status_type IN (1, 8)
    WHERE t.closed = true
        AND t.category_id IN ([CATEGORY_IDS])
        AND t.deleted_at IS NULL
        AND tt.execute_at IS NOT NULL
        AND tt.execute_at <= (CURRENT_DATE - INTERVAL '[TIME_PERIOD]')
    ORDER BY tt.execute_at DESC
)
    
SELECT '' AS total, topic_id, closed_at 
FROM topic_list
UNION
SELECT ''||COUNT(*), 0, CURRENT_DATE
FROM topic_list
ORDER BY closed_at DESC
Combined SQL + Script
sql = "WITH topic_list AS (
    SELECT 
        t.id AS topic_id, 
        tt.execute_at AS closed_at
    FROM topics t
    INNER JOIN categories c ON c.id = t.category_id
    LEFT JOIN topic_timers tt ON tt.topic_id = t.id AND tt.status_type IN (1, 8)
    WHERE t.closed = true
        AND t.category_id IN ([CATEGORY_IDS])
        AND t.deleted_at IS NULL
        AND tt.execute_at IS NOT NULL
        AND tt.execute_at <= (CURRENT_DATE - INTERVAL '[TIME_PERIOD]')
    ORDER BY tt.execute_at DESC
)
    
SELECT '' AS total, topic_id, closed_at 
FROM topic_list
UNION
SELECT ''||COUNT(*), 0, CURRENT_DATE
FROM topic_list
ORDER BY closed_at DESC"

results = ActiveRecord::Base.connection.execute(sql)
user = Discourse.system_user
archive_category = Category.find([ARCHIVE_CATEGORY_ID])

puts "Found #{results.count} topics to process"

results.each do |row|
    begin
    topic = Topic.find(row["topic_id"])
    
    # 1. Move to archive category
    topic.update!(category_id: archive_category.id)
    puts "#{topic.id} moved to archive category"
    
    # 2. Unlist the topic
    topic.update_status('visible', false, user, until: nil)
    puts "#{topic.id} is unlisted"

    # Error Handling 
    rescue => e
      puts "Error processing topic #{row["topic_id"]}: #{e.message}"
    end
end

puts "Process completed"

Destructive rake tasks


Delete entire categories

The following will allow you to destroy multiple categories, along with any subcategories and topics that belong to those categories.

Print out a list of category IDs

rake categories:list

Destroy a set of categories based on their ID

rake destroy:categories[10,11,12,18,30]

Delete all topics in a category

Remove all private messages

rake destroy:private_messages

Destroy all groups

rake destroy:groups

Destroy all non-admin users

rake destroy:users

Destroy site stats

rake destroy:stats

Anonymize all users except staff

rake users:anonymize_all

Permanently delete a set of posts

The following rake task will hard-delete a list of posts based on their ID. If a post is the first post in a topic, all posts in that topic will be hard-deleted. Before you can successfully run the task, the hidden can_permanently_delete site setting must be enabled via the rails console.

:warning: Once a post is deleted by this task, it will no longer exist in the database and cannot be undeleted.

There are two possible approaches:

  • Option 1 – Pass a comma-separated list of post IDs as an argument

    rake destroy:posts[4,8,15,16,23,42]
    
  • Option 2 – Specify a text file with a comma-separated list of post IDs (ideal for large sets of posts).

    cat post_ids.txt | rake destroy:posts
    

I’ve tried to include the most useful rake tasks in this topic, but there are many others packaged with Discourse. If you would like to see a comprehensive list, you can use the following:

All tasks that have descriptions

rake --tasks

All tasks, including those that do not have descriptions

rake -AT
70 Me gusta
Performing bulk actions as a moderator
Ruby on rails console command
Backup only Site Settings
How to move all topics of 1 category to new tag
Error Message when editing large number of topics
How To Apply signature to every user?
Delete/Remove All Tags in Bulk
Site rearrangement scripting: What's the best venue?
Recover deleted categories
How can I directly edit Discourse database from a GUI?
Bulk open closed topics
Suggest favorite YouTube tutorials for Discourse Admins and Moderators
Error Message when editing large number of topics
How to truncate bounced email log
Is there a way to grant badge to a group of user?
What makes a successful volunteer Discourse sysadmin?
How to extend time of all suspended users by one command?
Is there a bulk administrative command to aprove users?
DiscoTOC mobile view does not work with Docs
How might we better structure #howto?
Admin guide to tags in Discourse
Moving from sub-categories to tags?
Moving from sub-categories to tags?
Automatic Admin settings backups
Setting the api key via console
Bulk Action on Users
Mass-edit (or review) category parameters
How to activate and unstage accounts for imported users
Hide Muted Categories
Moving a group to TL3
Bulk Category Creation via CSV
Adding bulk categories
Can I/how to convert a tag to a category?
Rake post without confirmation
A few questions about batch moving or deleting topics
Restoring Deleted Messages?
What makes a successful volunteer Discourse sysadmin?
Understanding and using badges
How to recalculate all trust_levels?
Adding many categories
How to Delete Active Users whose Readtime is less or not logged in since long or not visited since long?
How to Reset Site Statistics or Recalculate?
A community template emphasizing place and intent
Docker attach gives empty new line
Change notification settings for all users per topic
Why is there a category that I can't modify
How to sort topics in Latest by creation date?
"Period" in tag
Bulk move many topics from one category to another
Can I disable choosing a default homepage in users' Interface settings?
Bulk delete all topics in a category
Update all users email watchlist with a (newly created) category
How to change owner of invite links?
Trust level names in spanish
Admin guide to tags in Discourse
Assign users a random digest time?
How to change language of all users?
Requesting help demoting users with invalid email addresses
Rake import category doesn't respect parent category
Is there a non-query way to remove a bulk of users from groups?
Set up file and image uploads to S3
Modify trust level for all users
Create user without sending invite
Removing specific staff action logs using the console
Is it possible to make database changes using postgres rather than rake?
Applying Watched Words to existing posts
Bulk changing subcategories doesn't select all topics?
How to update and unlock all groups?
CLI Command for Deleting Category
Apply preferences settings for all normal users globally
Switch all users to TL2
How to delete EVERYTHING from my forum and just leave the users?
Bulk mark messages and posters as spam
Bulk mark messages and posters as spam
Staff category permission change
Convert all existing topics in category to wikis
Migrate a phpBB3 forum to Discourse
Manage multiple Discourse instances from one place only
Merge categories
Automatically replacing some tags with categories on existing forum
Mass closing topics
User monetization with group access?
A way to trigger auto-tagging across all topics for a new watched word
Convert a category to private and add users to a specific group with access
How to mass remove PMs?
RSS Polling
How to delete thousands of Personal Messages?
How to change topic date after import
Mass Revoking Badges
How to turn off forum except for staff
Bulk changing ownership on category base
Bulk changing ownership on category base
Cannot delete tag with 2k topics
Is it possible to limit TL0 to posting in one category?
Mass close existing topics older than x?
Mass close existing topics older than x?
How to import category with category permissions?
Bulk open closed topics
How to clear all historical statistics from dashboard
How do I back up only forum configuration information?
Question about category populating with certain tags
How extensible can I make my installation in terms of sub domains and restricting membership to them?
Automatically re-categorize topics from an imported forum by targeting keywords in titles?
Emoji Fluff
Create a category that's private to one group, but open to other groups
自定义表情符号能否支持批量删除?
Unable to Bulk Suspend/Deactivate Users in Rails Console on Discourse 3.5.0.beta5-dev
How to Disable ALL User-to-User DMs/Chat without breaking Other Features?
Move Some topics to New Instance
Structure and duplication of category contents intra discourse and between discourse sites
Is there a way to convert categories into tags?
Get Error Oops The software powering this discussion forum encountered an unexpected problem after upgrade
Export/import theme components in bulk?
Need To Reopen All Topics!
How to bulk delete old whispers
How to get rid of thousands of topics that have no value?
Add a tag to all topics in a category
How to Create a Script to "Bulk Remove" from a Group?
How to delete all warnings?
Marking multiple account as Suspended account
How can I directly edit Discourse database from a GUI?
Enable Badge SQL
Are conditions to get new Trust level cumulative?
Bulk Remove Members from a Group
Can't reverse silencing a category for a group
Assigning mod status to multiple users
Preserving site settings through migration
Limit volume of forum emails via admin
Is it possible to import a Discourse backup as a category inside an alredy existing discourse isntance?