mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
FEATURE: export/import topics and categories from one Discourse site to another. (Early-access alpha greenlight. More to do...)
This commit is contained in:
parent
0337964759
commit
58610d15a1
6 changed files with 346 additions and 0 deletions
59
lib/import_export/category_exporter.rb
Normal file
59
lib/import_export/category_exporter.rb
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
module ImportExport
|
||||||
|
class CategoryExporter
|
||||||
|
|
||||||
|
attr_reader :export_data
|
||||||
|
|
||||||
|
def initialize(category_id)
|
||||||
|
@category = Category.find(category_id)
|
||||||
|
@subcategories = Category.where(parent_category_id: category_id)
|
||||||
|
@export_data = {
|
||||||
|
users: [],
|
||||||
|
category: nil,
|
||||||
|
subcategories: [],
|
||||||
|
topics: []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
puts "Exporting category #{@category.name}...", ""
|
||||||
|
export_categories
|
||||||
|
export_topics_and_users
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
CATEGORY_ATTRS = [:id, :name, :color, :created_at, :user_id, :slug, :description, :text_color,
|
||||||
|
:auto_close_hours, :logo_url, :background_url, :auto_close_based_on_last_post,
|
||||||
|
:topic_template, :suppress_from_homepage]
|
||||||
|
|
||||||
|
def export_categories
|
||||||
|
# description
|
||||||
|
@export_data[:category] = CATEGORY_ATTRS.inject({}) { |h,a| h[a] = @category.send(a); h }
|
||||||
|
@subcategories.find_each do |subcat|
|
||||||
|
@export_data[:subcategories] << CATEGORY_ATTRS.inject({}) { |h,a| h[a] = subcat.send(a); h }
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_topics_and_users
|
||||||
|
all_category_ids = [@category.id] + @subcategories.pluck(:id)
|
||||||
|
description_topic_ids = Category.where(id: all_category_ids).pluck(:topic_id)
|
||||||
|
topic_exporter = ImportExport::TopicExporter.new(Topic.where(category_id: all_category_ids).pluck(:id) - description_topic_ids)
|
||||||
|
topic_exporter.perform
|
||||||
|
@export_data[:users] = topic_exporter.export_data[:users]
|
||||||
|
@export_data[:topics] = topic_exporter.export_data[:topics]
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_to_file(filename=nil)
|
||||||
|
require 'json'
|
||||||
|
output_basename = filename || File.join("category-export-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json")
|
||||||
|
File.open(output_basename, "w:UTF-8") do |f|
|
||||||
|
f.write(@export_data.to_json)
|
||||||
|
end
|
||||||
|
puts "Export saved to #{output_basename}"
|
||||||
|
output_basename
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
55
lib/import_export/category_importer.rb
Normal file
55
lib/import_export/category_importer.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
require File.join(Rails.root, 'script', 'import_scripts', 'base.rb')
|
||||||
|
|
||||||
|
module ImportExport
|
||||||
|
class CategoryImporter < ImportScripts::Base
|
||||||
|
def initialize(export_data)
|
||||||
|
@export_data = export_data
|
||||||
|
@topic_importer = TopicImporter.new(@export_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
RateLimiter.disable
|
||||||
|
|
||||||
|
import_users
|
||||||
|
import_categories
|
||||||
|
import_topics
|
||||||
|
self
|
||||||
|
ensure
|
||||||
|
RateLimiter.enable
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_users
|
||||||
|
@topic_importer.import_users
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_categories
|
||||||
|
id = @export_data[:category].delete(:id)
|
||||||
|
parent = Category.new(@export_data[:category])
|
||||||
|
parent.user_id = @topic_importer.new_user_id(@export_data[:category][:user_id]) # imported user's new id
|
||||||
|
parent.custom_fields["import_id"] = id
|
||||||
|
parent.save!
|
||||||
|
set_category_description(parent, @export_data[:category][:description])
|
||||||
|
|
||||||
|
@export_data[:subcategories].each do |cat_attrs|
|
||||||
|
id = cat_attrs.delete(:id)
|
||||||
|
subcategory = Category.new(cat_attrs)
|
||||||
|
subcategory.parent_category_id = parent.id
|
||||||
|
subcategory.user_id = @topic_importer.new_user_id(cat_attrs[:user_id])
|
||||||
|
subcategory.custom_fields["import_id"] = id
|
||||||
|
subcategory.save!
|
||||||
|
set_category_description(subcategory, cat_attrs[:description])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_category_description(c, description)
|
||||||
|
post = c.topic.ordered_posts.first
|
||||||
|
post.raw = description
|
||||||
|
post.save!
|
||||||
|
post.rebake!
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_topics
|
||||||
|
@topic_importer.import_topics
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
26
lib/import_export/import_export.rb
Normal file
26
lib/import_export/import_export.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
require "import_export/category_exporter"
|
||||||
|
require "import_export/category_importer"
|
||||||
|
require "import_export/topic_exporter"
|
||||||
|
require "import_export/topic_importer"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
module ImportExport
|
||||||
|
|
||||||
|
def self.export_category(category_id)
|
||||||
|
ImportExport::CategoryExporter.new(category_id).perform.save_to_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.import_category(filename)
|
||||||
|
export_data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) })
|
||||||
|
ImportExport::CategoryImporter.new(export_data).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.export_topics(topic_ids)
|
||||||
|
ImportExport::TopicExporter.new(topic_ids).perform.save_to_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.import_topics(filename)
|
||||||
|
export_data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) })
|
||||||
|
ImportExport::TopicImporter.new(export_data).perform
|
||||||
|
end
|
||||||
|
end
|
94
lib/import_export/topic_exporter.rb
Normal file
94
lib/import_export/topic_exporter.rb
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
module ImportExport
|
||||||
|
class TopicExporter
|
||||||
|
|
||||||
|
attr_reader :exported_user_ids, :export_data
|
||||||
|
|
||||||
|
def initialize(topic_ids)
|
||||||
|
@topic_ids = topic_ids
|
||||||
|
@exported_user_ids = []
|
||||||
|
@export_data = {
|
||||||
|
users: [],
|
||||||
|
topics: []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
export_users
|
||||||
|
export_topics
|
||||||
|
# TODO: user actions
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
USER_ATTRS = [:id, :email, :username, :name, :created_at, :trust_level, :active, :last_emailed_at]
|
||||||
|
|
||||||
|
def export_users
|
||||||
|
# TODO: avatar
|
||||||
|
|
||||||
|
@exported_user_ids = []
|
||||||
|
@topic_ids.each do |topic_id|
|
||||||
|
t = Topic.find(topic_id)
|
||||||
|
t.posts.includes(user: [:user_profile]).find_each do |post|
|
||||||
|
u = post.user
|
||||||
|
unless @exported_user_ids.include?(u.id)
|
||||||
|
x = USER_ATTRS.inject({}) { |h, a| h[a] = u.send(a); h; }
|
||||||
|
@export_data[:users] << x.merge({
|
||||||
|
bio_raw: u.user_profile.bio_raw,
|
||||||
|
website: u.user_profile.website,
|
||||||
|
location: u.user_profile.location
|
||||||
|
})
|
||||||
|
@exported_user_ids << u.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def export_topics
|
||||||
|
@topic_ids.each do |topic_id|
|
||||||
|
t = Topic.find(topic_id)
|
||||||
|
puts t.title
|
||||||
|
export_topic(t)
|
||||||
|
end
|
||||||
|
puts ""
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
TOPIC_ATTRS = [:id, :title, :created_at, :views, :category_id, :closed, :archived, :archetype]
|
||||||
|
POST_ATTRS = [:id, :user_id, :post_number, :raw, :created_at, :reply_to_post_number,
|
||||||
|
:hidden, :hidden_reason_id, :wiki]
|
||||||
|
|
||||||
|
def export_topic(topic)
|
||||||
|
topic_data = {}
|
||||||
|
|
||||||
|
TOPIC_ATTRS.each do |a|
|
||||||
|
topic_data[a] = topic.send(a)
|
||||||
|
end
|
||||||
|
|
||||||
|
topic_data[:posts] = []
|
||||||
|
|
||||||
|
topic.ordered_posts.find_each do |post|
|
||||||
|
topic_data[:posts] << POST_ATTRS.inject({}) { |h, a| h[a] = post.send(a); h; }
|
||||||
|
end
|
||||||
|
|
||||||
|
@export_data[:topics] << topic_data
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_file(filename=nil)
|
||||||
|
require 'json'
|
||||||
|
output_basename = filename || File.join("topic-export-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json")
|
||||||
|
File.open(output_basename, "w:UTF-8") do |f|
|
||||||
|
f.write(@export_data.to_json)
|
||||||
|
end
|
||||||
|
puts "Export saved to #{output_basename}"
|
||||||
|
output_basename
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
67
lib/import_export/topic_importer.rb
Normal file
67
lib/import_export/topic_importer.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
require File.join(Rails.root, 'script', 'import_scripts', 'base.rb')
|
||||||
|
|
||||||
|
module ImportExport
|
||||||
|
class TopicImporter < ImportScripts::Base
|
||||||
|
def initialize(export_data)
|
||||||
|
@export_data = export_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
RateLimiter.disable
|
||||||
|
|
||||||
|
import_users
|
||||||
|
import_topics
|
||||||
|
self
|
||||||
|
ensure
|
||||||
|
RateLimiter.enable
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_users
|
||||||
|
@export_data[:users].each do |u|
|
||||||
|
existing = User.where(email: u[:email]).first
|
||||||
|
if existing && existing.custom_fields["import_id"] != u[:id]
|
||||||
|
existing.custom_fields["import_id"] = u[:id]
|
||||||
|
existing.save!
|
||||||
|
else
|
||||||
|
u = create_user(u, u[:id]) # see ImportScripts::Base
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_topics
|
||||||
|
@export_data[:topics].each do |t|
|
||||||
|
puts ""
|
||||||
|
print t[:title]
|
||||||
|
|
||||||
|
first_post_attrs = t[:posts].first.merge( t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id])) )
|
||||||
|
first_post_attrs[:user_id] = new_user_id(first_post_attrs[:user_id])
|
||||||
|
first_post_attrs[:category] = new_category_id(t[:category_id])
|
||||||
|
|
||||||
|
first_post = create_post( first_post_attrs, first_post_attrs[:id] )
|
||||||
|
topic_id = first_post.topic_id
|
||||||
|
t[:posts].each_with_index do |post_data, i|
|
||||||
|
next if i == 0
|
||||||
|
print "."
|
||||||
|
create_post(post_data.merge({
|
||||||
|
topic_id: topic_id,
|
||||||
|
user_id: new_user_id(post_data[:user_id])
|
||||||
|
}), post_data[:id]) # see ImportScripts::Base
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_user_id(external_user_id)
|
||||||
|
ucf = UserCustomField.where(name: "import_id", value: external_user_id.to_s).first
|
||||||
|
ucf ? ucf.user_id : Discourse::SYSTEM_USER_ID
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_category_id(external_category_id)
|
||||||
|
CategoryCustomField.where(name: "import_id", value: external_category_id).first.category_id rescue nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -134,12 +134,57 @@ class DiscourseCLI < Thor
|
||||||
puts 'Requests sent. Clients will refresh on next navigation.'
|
puts 'Requests sent. Clients will refresh on next navigation.'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "export_category", "Export a category, all its topics, and all users who posted in those topics"
|
||||||
|
def export_category(category_id)
|
||||||
|
raise "Category id argument is missing!" unless category_id
|
||||||
|
|
||||||
|
load_rails
|
||||||
|
load_import_export
|
||||||
|
ImportExport.export_category(category_id)
|
||||||
|
puts "", "Done", ""
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "import_category", "Import a category, its topics and the users from the output of the export_category command"
|
||||||
|
def import_category(filename)
|
||||||
|
raise "File name argument missing!" unless filename
|
||||||
|
|
||||||
|
puts "Starting import from #{filename}..."
|
||||||
|
load_rails
|
||||||
|
load_import_export
|
||||||
|
ImportExport.import_category(filename)
|
||||||
|
puts "", "Done", ""
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "export_topics", "Export topics and all users who posted in that topic. Accepts multiple topic id's"
|
||||||
|
def export_topics(*topic_ids)
|
||||||
|
puts "Starting export of topics...", ""
|
||||||
|
load_rails
|
||||||
|
load_import_export
|
||||||
|
ImportExport.export_topics(topic_ids)
|
||||||
|
puts "", "Done", ""
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "import_topics", "Import topics and their users from the output of the export_topic command"
|
||||||
|
def import_topics(filename)
|
||||||
|
raise "File name argument missing!" unless filename
|
||||||
|
|
||||||
|
puts "Starting import from #{filename}..."
|
||||||
|
load_rails
|
||||||
|
load_import_export
|
||||||
|
ImportExport.import_topics(filename)
|
||||||
|
puts "", "Done", ""
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_rails
|
def load_rails
|
||||||
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
|
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_import_export
|
||||||
|
require File.expand_path(File.dirname(__FILE__) + "/../lib/import_export/import_export")
|
||||||
|
end
|
||||||
|
|
||||||
def do_remap(from, to)
|
def do_remap(from, to)
|
||||||
sql = "SELECT table_name, column_name
|
sql = "SELECT table_name, column_name
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
|
|
Loading…
Reference in a new issue