diff --git a/app/models/account/data_transfer/entropy_record_set.rb b/app/models/account/data_transfer/entropy_record_set.rb new file mode 100644 index 000000000..e620a8121 --- /dev/null +++ b/app/models/account/data_transfer/entropy_record_set.rb @@ -0,0 +1,32 @@ +class Account::DataTransfer::EntropyRecordSet < Account::DataTransfer::RecordSet + def initialize(account) + super(account: account, model: Entropy) + end + + private + def import_batch(files) + batch_data = files.map do |file| + data = load(file) + data.slice(*attributes).merge("account_id" => account.id) + end + + container_keys = batch_data.map { |d| [ d["container_type"], d["container_id"] ] } + existing_containers = Entropy + .where(account_id: account.id) + .where(container_type: container_keys.map(&:first), container_id: container_keys.map(&:last)) + .pluck(:container_type, :container_id) + .to_set + + to_update, to_insert = batch_data.partition do |data| + existing_containers.include?([ data["container_type"], data["container_id"] ]) + end + + to_update.each do |data| + Entropy + .find_by(account_id: account.id, container_type: data["container_type"], container_id: data["container_id"]) + .update!(data.slice("auto_postpone_period")) + end + + Entropy.insert_all!(to_insert) if to_insert.any? + end +end diff --git a/app/models/account/data_transfer/manifest.rb b/app/models/account/data_transfer/manifest.rb index ffa65ad2f..e764f69c1 100644 --- a/app/models/account/data_transfer/manifest.rb +++ b/app/models/account/data_transfer/manifest.rb @@ -20,10 +20,11 @@ class Account::DataTransfer::Manifest [ Account::DataTransfer::AccountRecordSet.new(account), Account::DataTransfer::UserRecordSet.new(account), + Account::DataTransfer::RecordSet.new(account: account, model: ::User::Settings), Account::DataTransfer::RecordSet.new(account: account, model: ::Tag), Account::DataTransfer::RecordSet.new(account: account, model: ::Board), Account::DataTransfer::RecordSet.new(account: account, model: ::Column), - Account::DataTransfer::RecordSet.new(account: account, model: ::Entropy), + Account::DataTransfer::EntropyRecordSet.new(account), Account::DataTransfer::RecordSet.new(account: account, model: ::Board::Publication), Account::DataTransfer::RecordSet.new(account: account, model: ::Webhook), Account::DataTransfer::RecordSet.new(account: account, model: ::Access), diff --git a/app/models/account/data_transfer/record_set.rb b/app/models/account/data_transfer/record_set.rb index c2dc09426..b3fd9d5c1 100644 --- a/app/models/account/data_transfer/record_set.rb +++ b/app/models/account/data_transfer/record_set.rb @@ -87,6 +87,16 @@ class Account::DataTransfer::RecordSet if model.exists?(id: data["id"]) raise IntegrityError, "#{model} record with ID #{data['id']} already exists" end + + record = model.new(data.slice(*attributes).merge("account_id" => account.id)) + belongs_to_associations = model.reflect_on_all_associations(:belongs_to).map { |a| a.name.to_s } + + record.validate + errors = record.errors.reject { |e| belongs_to_associations.include?(e.attribute.to_s) } + + if errors.any? + raise IntegrityError, "Validation failed for #{model} record ID #{data['id']}: #{errors.map(&:full_message).join(', ')}" + end end def load(file_path) diff --git a/app/models/account/data_transfer/user_record_set.rb b/app/models/account/data_transfer/user_record_set.rb index 277dc82c3..a0a140f2d 100644 --- a/app/models/account/data_transfer/user_record_set.rb +++ b/app/models/account/data_transfer/user_record_set.rb @@ -40,6 +40,9 @@ class Account::DataTransfer::UserRecordSet < Account::DataTransfer::RecordSet ) end + conflicting_identity_ids = batch_data.pluck("identity_id").compact + account.users.where(identity_id: conflicting_identity_ids).destroy_all + User.insert_all!(batch_data) end diff --git a/app/models/account/import.rb b/app/models/account/import.rb index a68a1b37b..1e98ce2e1 100644 --- a/app/models/account/import.rb +++ b/app/models/account/import.rb @@ -1,4 +1,6 @@ class Account::Import < ApplicationRecord + broadcasts_refreshes + belongs_to :account belongs_to :identity @@ -7,6 +9,7 @@ class Account::Import < ApplicationRecord enum :status, %w[ pending processing completed failed ].index_by(&:itself), default: :pending def process_later + ImportAccountDataJob.perform_later(self) end def process(start: nil, callback: nil) diff --git a/test/models/account/import_test.rb b/test/models/account/import_test.rb index 1abada2a3..2d357da1f 100644 --- a/test/models/account/import_test.rb +++ b/test/models/account/import_test.rb @@ -93,6 +93,45 @@ class Account::ImportTest < ActiveSupport::TestCase assert import.completed? end + test "export and import round-trip preserves account data" do + source_account = accounts("37s") + exporter = users(:david) + identity = exporter.identity + + # Capture original counts + original_counts = capture_counts(source_account) + + # Export + export = Account::Export.create!(account: source_account, user: exporter) + export.build + assert export.completed? + + # Save export file before deleting account + export_tempfile = Tempfile.new([ "export", ".zip" ]) + export.file.open { |f| FileUtils.cp(f.path, export_tempfile.path) } + + # Delete source account + source_account.destroy! + + # Create target account + target_account = Account.create!(name: "Import Target") + target_account.users.create!(role: :system, name: "System") + target_account.users.create!(role: :owner, name: "Owner", identity: identity, verified_at: Time.current) + + # Import + import = Account::Import.create!(identity: identity, account: target_account) + Current.set(account: target_account) do + import.file.attach(io: File.open(export_tempfile.path), filename: "export.zip", content_type: "application/zip") + end + import.process + assert import.completed? + + assert_equal original_counts.except(:users), capture_counts(target_account).except(:users) + ensure + export_tempfile&.close + export_tempfile&.unlink + end + private def create_target_account account = Account.create!(name: "Import Target") @@ -202,4 +241,15 @@ class Account::ImportTest < ActiveSupport::TestCase end File.open(tempfile.path, "rb") end + + def capture_counts(account) + { + boards: account.boards.count, + columns: Column.joins(:board).where(boards: { account_id: account.id }).count, + cards: account.cards.count, + comments: Comment.joins(:card).where(cards: { account_id: account.id }).count, + tags: account.tags.count, + users: account.users.count + } + end end