# frozen_string_literal: true namespace :portal do # rubocop:disable Metrics/BlockLength desc 'Clone a help center portal with all its categories, folders, and articles. Optionally clone to another account.' task :clone, %i[portal_id target_account_id] => :environment do |_task, args| # rubocop:disable Metrics/BlockLength portal_id = args[:portal_id] target_account_id = args[:target_account_id] if portal_id.blank? puts 'Usage: rails portal:clone[] or rails portal:clone[,]' next end source_portal = Portal.find_by(id: portal_id) unless source_portal puts "ERROR: Portal with ID #{portal_id} not found." next end target_account = if target_account_id.present? Account.find_by(id: target_account_id).tap do |account| unless account puts "ERROR: Account with ID #{target_account_id} not found." next end end else source_portal.account end next unless target_account cross_account = target_account.id != source_portal.account_id if cross_account default_author = target_account.administrators.first || target_account.agents.first unless default_author puts "ERROR: Target account #{target_account.id} has no administrators or agents to assign as article author." next end puts "Cross-account clone: articles will be authored by #{default_author.name} (ID: #{default_author.id})" end puts "Cloning portal '#{source_portal.name}' (ID: #{source_portal.id}) to account #{target_account.id}..." old_to_new_category = {} old_to_new_folder = {} old_to_new_article = {} ActiveRecord::Base.transaction do # rubocop:disable Metrics/BlockLength # 1. Clone the portal new_slug = "#{source_portal.slug}-copy-#{Time.now.utc.to_i}" new_portal = source_portal.dup new_portal.slug = new_slug new_portal.custom_domain = nil new_portal.channel_web_widget_id = nil new_portal.account_id = target_account.id new_portal.name = "#{source_portal.name} (Copy)" new_portal.save! puts "Created portal '#{new_portal.name}' (ID: #{new_portal.id}, slug: #{new_portal.slug})" # Copy logo if present if source_portal.logo.attached? new_portal.logo.attach(source_portal.logo.blob) puts 'Copied portal logo.' end # 2. Clone categories (without self-referential FKs first) puts 'Cloning categories...' source_portal.categories.find_each do |category| new_category = category.dup new_category.portal_id = new_portal.id new_category.parent_category_id = nil new_category.associated_category_id = nil new_category.save! old_to_new_category[category.id] = new_category.id end puts "Cloned #{old_to_new_category.count} categories." # 3. Remap category self-referential relationships source_portal.categories.where.not(parent_category_id: nil).find_each do |category| new_parent_id = old_to_new_category[category.parent_category_id] next unless new_parent_id new_category = Category.find(old_to_new_category[category.id]) new_category.update!(parent_category_id: new_parent_id) end source_portal.categories.where.not(associated_category_id: nil).find_each do |category| new_assoc_id = old_to_new_category[category.associated_category_id] next unless new_assoc_id new_category = Category.find(old_to_new_category[category.id]) new_category.update!(associated_category_id: new_assoc_id) end # 4. Clone related categories (junction table) related_count = 0 source_portal.categories.find_each do |category| category.category_related_categories.find_each do |rc| new_category_id = old_to_new_category[rc.category_id] new_related_id = old_to_new_category[rc.related_category_id] next unless new_category_id && new_related_id RelatedCategory.create!(category_id: new_category_id, related_category_id: new_related_id) related_count += 1 end end puts "Cloned #{related_count} related category links." if related_count.positive? # 5. Clone folders puts 'Cloning folders...' source_portal.folders.find_each do |folder| new_folder = folder.dup new_folder.category_id = old_to_new_category[folder.category_id] new_folder.save! old_to_new_folder[folder.id] = new_folder.id end puts "Cloned #{old_to_new_folder.count} folders." # 6. Clone articles (without associated_article_id first) puts 'Cloning articles...' source_portal.articles.find_each do |article| new_article = article.dup new_article.portal_id = new_portal.id new_article.category_id = old_to_new_category[article.category_id] if article.category_id new_article.folder_id = old_to_new_folder[article.folder_id] if article.folder_id new_article.author_id = default_author.id if cross_account new_article.associated_article_id = nil new_article.slug = "#{Time.now.utc.to_i}-#{SecureRandom.hex(4)}-#{article.slug.last(100)}" new_article.views = 0 new_article.save! old_to_new_article[article.id] = new_article.id end puts "Cloned #{old_to_new_article.count} articles." # 7. Remap article self-referential relationships source_portal.articles.where.not(associated_article_id: nil).find_each do |article| new_assoc_id = old_to_new_article[article.associated_article_id] next unless new_assoc_id new_article = Article.find(old_to_new_article[article.id]) new_article.update!(associated_article_id: new_assoc_id) end puts "\nCloning complete!" puts "New portal ID: #{new_portal.id}" puts "New portal slug: #{new_portal.slug}" puts "Summary: #{old_to_new_category.count} categories, #{old_to_new_folder.count} folders, #{old_to_new_article.count} articles" end rescue ActiveRecord::RecordNotFound puts "ERROR: Portal with ID #{portal_id} not found." rescue ActiveRecord::RecordInvalid => e puts "ERROR: Failed to clone portal. Validation error: #{e.message}" end end