Bulk convert topics from a Category to Group PMs

AI-generated summary

nathank sought a method to bulk convert topics from a specific category into Group PMs to deprecate unused categories, as the UI only supports one-by-one conversion. While pfaffman suggested using update_all in the Rails console for speed, nathank noted the complexity of managing topic_groups and topic_allowed_groups tables required a more robust solution. nathank ultimately detailed two successful approaches: a manual UI method and a powerful Rails Console script. The UI method involves temporarily removing group members to prevent notifications, converting topics via the admin menu, re-adding the group, and cleaning up. The Rails Console script automates the process by iterating through topics in a source category, changing their archetype to private_message, setting subtype to user_to_user, nullifying the category_id, and linking them to the target group via TopicAllowedGroup and TopicAllowedUser. This script preserves existing participants and avoids triggering notifications. nathank also provided a reverse script to migrate Group Inbox topics back to a category and noted that Jobs::CategoryStats may need manual execution to update statistics after deletion.

I plan to convert a bunch of topics to group PMs in order to depreciate some barely used categories. We can’t do that via the UI (only one by one), nor can I find a bulk way to do it.

As far as I can tell this should work via the console:

  1. Identify the topics by their category
  2. Change topic.archetype to private_message
  3. Change topic.subtype to ‘user_to_user’
  4. Make topic.category Null
  5. In topic_groups (or maybe topic_allowed_groups), add a line with the relevant group_id and topic_id

Does this approach make sense? I need to work out the difference between those topic/groups tables first of course. And do it in a sandbox for starters!!

That could work. Also, if you don’t know you can do something like

 topics = Topic.where(category: 10)

  topics.update_all(archetype: xxx, topic_category: nil)

Or something like that. If you knew about update_all, this isn’t news.

Thanks Jay! That would be nice and fast for the first half. However, I also have to ensure that they end up as Group PMs.

Any idea what I have to do with the topic_groups / topic_allowed_groups tables? I don’t think that I can get away with an update_all for them.

I’ve since then (clearly took a while!) worked out two ways to achieve this. You can do it via the UI, which works well for small Categories (and small groups). It takes a bit of care to avoid firing off unhelpful notifications while you do so, but is very doable.

Alternatively, you can use the Rails Console (assuming you are self-hosted), which is faster and much more powerful.

From the UI

  1. Remove the other group members from the Group (but keep a list of usernames / emails so you can re-add them easily). This will avoid them being notified.
  2. Cycle through each topic in the Category, converting each one into a PM using the admin spanner menu
    • I find this easiest to do by opening each topic in its own tab, and then moving across them one by one
  3. Add the Group to each of the Topics (now PMs)
  4. Add the Group members back into the group
  5. Tidy up / delete all the ‘small posts’ in each Group Message as desired

In the Rails Console

Access the console by entering the container and then calling up the console:

./launcher enter app
  rails c

Set the Category and Group, and then copy the script below into the console:

category_id = =CategoryID= 
group_name = '=GroupName='

group = Group.find_by('lower(name) = ?', group_name.downcase)
about_id = Category.find(category_id).topic_id

topic_ids = Topic.where(category_id: category_id).where.not(id: about_id).pluck(:id)

Topic.where(id: topic_ids).find_each do |topic|
  topic.archetype = Archetype.private_message
  topic.category_id = nil
  topic.subtype = 'user_to_user'
  topic.save!

  TopicAllowedGroup.find_or_create_by!(topic_id: topic.id, group_id: group.id)

  Post.where(topic_id: topic.id).pluck(:user_id).uniq.each do |uid|
    TopicAllowedUser.find_or_create_by!(topic_id: topic.id, user_id: uid)
  end
end

This doesn’t notify the users at all, and preserves the users participating in the posts nicely (just in case they aren’t in the Group).

Of note, you might need to run this subsequently in the Rails Console if you want to delete the Category before the background job recounts everything:

Jobs::CategoryStats.new.execute(category_id: category_id)
Script for the reverse (Group Inbox to Category), click to reveal

This script should send things back in the other direction nicely (untested)

category_id = =CategoryID= 
group_name = '=GroupName='

group = Group.find_by('LOWER(name) = ?', group_name.downcase)
raise "Group not found" unless group

topic_ids = TopicAllowedGroup.where(group_id: group.id).pluck(:topic_id)
topics = Topic.where(id: topic_ids, archetype: Archetype.private_message, deleted_at: nil)

topics.find_each do |topic|
  topic.archetype = Archetype.default  # or "regular"
  topic.subtype = nil
  topic.category_id = target_category_id
  topic.save!

end