Split Export model into user and whole-account export

This commit is contained in:
Stanko K.R.
2025-12-12 17:55:25 +01:00
parent 63d9f29b1e
commit 6f17e3867d
9 changed files with 323 additions and 266 deletions
+17 -24
View File
@@ -19,7 +19,7 @@ class Account::Export < ApplicationRecord
def build
processing!
zipfile = generate_zip
zipfile = generate_zip { |zip| populate_zip(zip) }
file.attach io: File.open(zipfile.path), filename: "fizzy-export-#{id}.zip", content_type: "application/zip"
mark_completed
@@ -42,36 +42,29 @@ class Account::Export < ApplicationRecord
end
private
def populate_zip(zip)
raise NotImplementedError, "Subclasses must implement populate_zip"
end
def generate_zip
raise ArgumentError, "Block is required" unless block_given?
Tempfile.new([ "export", ".zip" ]).tap do |tempfile|
Zip::File.open(tempfile.path, create: true) do |zip|
exportable_cards.find_each do |card|
add_card_to_zip(zip, card)
end
yield zip
end
end
end
def exportable_cards
user.accessible_cards.includes(
:board,
creator: :identity,
comments: { creator: :identity },
rich_text_description: { embeds_attachments: :blob }
)
end
def add_card_to_zip(zip, card)
zip.get_output_stream("#{card.number}.json") do |f|
f.write(card.export_json)
end
card.export_attachments.each do |attachment|
zip.get_output_stream(attachment[:path], compression_method: Zip::Entry::STORED) do |f|
attachment[:blob].download { |chunk| f.write(chunk) }
end
rescue ActiveStorage::FileNotFoundError
# Skip attachments where the file is missing from storage
def add_file_to_zip(zip, path, content = nil, **options)
zip.get_output_stream(path, **options) do |f|
if block_given?
yield f
elsif content
f.write(content)
else
raise ArgumentError, "Either content or a block must be provided"
end
end
end
end
+29
View File
@@ -0,0 +1,29 @@
class Account::SingleUserExport < Account::Export
private
def populate_zip(zip)
exportable_cards.find_each do |card|
add_card_to_zip(zip, card)
end
end
def exportable_cards
user.accessible_cards.includes(
:board,
creator: :identity,
comments: { creator: :identity },
rich_text_description: { embeds_attachments: :blob }
)
end
def add_card_to_zip(zip, card)
add_file_to_zip(zip, "#{card.number}.json", card.export_json)
card.export_attachments.each do |attachment|
add_file_to_zip(zip, attachment[:path], compression_method: Zip::Entry::STORED) do |f|
attachment[:blob].download { |chunk| f.write(chunk) }
end
rescue ActiveStorage::FileNotFoundError
# Skip attachments where the file is missing from storage
end
end
end
@@ -0,0 +1,21 @@
class Account::WholeAccountExport < Account::Export
private
def populate_zip(zip)
export_account(zip)
export_users(zip)
end
def export_account(zip)
data = account.as_json.merge(
join_code: account.join_code.as_json,
)
add_file_to_zip(zip, "account.json", JSON.pretty_generate(data))
end
def export_users(zip)
account.users.find_each do |user|
add_file_to_zip(zip, "users/#{user.id}.json", user.export_json)
end
end
end
@@ -0,0 +1,5 @@
class AddTypeToAccountExports < ActiveRecord::Migration[8.2]
def change
add_column :account_exports, :type, :string
end
end
Generated
+1
View File
@@ -38,6 +38,7 @@ ActiveRecord::Schema[8.2].define(version: 2026_01_21_155752) do
t.datetime "completed_at"
t.datetime "created_at", null: false
t.string "status", default: "pending", null: false
t.string "type"
t.datetime "updated_at", null: false
t.uuid "user_id", null: false
t.index [ "account_id" ], name: "index_account_exports_on_account_id"
@@ -50,7 +50,7 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
end
test "show displays completed export with download link" do
export = Account::Export.create!(account: Current.account, user: users(:david))
export = Account::SingleUserExport.create!(account: Current.account, user: users(:david))
export.build
get account_export_path(export)
@@ -67,7 +67,7 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
end
test "show does not allow access to another user's export" do
export = Account::Export.create!(account: Current.account, user: users(:kevin))
export = Account::SingleUserExport.create!(account: Current.account, user: users(:kevin))
export.build
get account_export_path(export)
+5 -66
View File
@@ -2,42 +2,15 @@ require "test_helper"
class Account::ExportTest < ActiveSupport::TestCase
test "build_later enqueues ExportAccountDataJob" do
export = Account::Export.create!(account: Current.account, user: users(:david))
export = Account::SingleUserExport.create!(account: Current.account, user: users(:david))
assert_enqueued_with(job: ExportAccountDataJob, args: [ export ]) do
export.build_later
end
end
test "build generates zip with card JSON files" do
export = Account::Export.create!(account: Current.account, user: users(:david))
export.build
assert export.completed?
assert export.file.attached?
assert_equal "application/zip", export.file.content_type
end
test "build sets status to processing then completed" do
export = Account::Export.create!(account: Current.account, user: users(:david))
export.build
assert export.completed?
assert_not_nil export.completed_at
end
test "build sends email when completed" do
export = Account::Export.create!(account: Current.account, user: users(:david))
assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do
export.build
end
end
test "build sets status to failed on error" do
export = Account::Export.create!(account: Current.account, user: users(:david))
export = Account::SingleUserExport.create!(account: Current.account, user: users(:david))
export.stubs(:generate_zip).raises(StandardError.new("Test error"))
assert_raises(StandardError) do
@@ -48,9 +21,9 @@ class Account::ExportTest < ActiveSupport::TestCase
end
test "cleanup deletes exports completed more than 24 hours ago" do
old_export = Account::Export.create!(account: Current.account, user: users(:david), status: :completed, completed_at: 25.hours.ago)
recent_export = Account::Export.create!(account: Current.account, user: users(:david), status: :completed, completed_at: 23.hours.ago)
pending_export = Account::Export.create!(account: Current.account, user: users(:david), status: :pending)
old_export = Account::SingleUserExport.create!(account: Current.account, user: users(:david), status: :completed, completed_at: 25.hours.ago)
recent_export = Account::SingleUserExport.create!(account: Current.account, user: users(:david), status: :completed, completed_at: 23.hours.ago)
pending_export = Account::SingleUserExport.create!(account: Current.account, user: users(:david), status: :pending)
Account::Export.cleanup
@@ -58,38 +31,4 @@ class Account::ExportTest < ActiveSupport::TestCase
assert Account::Export.exists?(recent_export.id)
assert Account::Export.exists?(pending_export.id)
end
test "build includes only accessible cards for user" do
user = users(:david)
export = Account::Export.create!(account: Current.account, user: user)
export.build
assert export.completed?
assert export.file.attached?
# Verify zip contents
Tempfile.create([ "test", ".zip" ]) do |temp|
temp.binmode
export.file.download { |chunk| temp.write(chunk) }
temp.rewind
Zip::File.open(temp.path) do |zip|
json_files = zip.glob("*.json")
assert json_files.any?, "Zip should contain at least one JSON file"
# Verify structure of a JSON file
json_content = JSON.parse(zip.read(json_files.first.name))
assert json_content.key?("number")
assert json_content.key?("title")
assert json_content.key?("board")
assert json_content.key?("creator")
assert json_content["creator"].key?("id")
assert json_content["creator"].key?("name")
assert json_content["creator"].key?("email")
assert json_content.key?("description")
assert json_content.key?("comments")
end
end
end
end
@@ -0,0 +1,62 @@
require "test_helper"
class Account::SingleUserExportTest < ActiveSupport::TestCase
test "build generates zip with card JSON files" do
export = Account::SingleUserExport.create!(account: Current.account, user: users(:david))
export.build
assert export.completed?
assert export.file.attached?
assert_equal "application/zip", export.file.content_type
end
test "build sets status to processing then completed" do
export = Account::SingleUserExport.create!(account: Current.account, user: users(:david))
export.build
assert export.completed?
assert_not_nil export.completed_at
end
test "build sends email when completed" do
export = Account::SingleUserExport.create!(account: Current.account, user: users(:david))
assert_enqueued_jobs 1, only: ActionMailer::MailDeliveryJob do
export.build
end
end
test "build includes only accessible cards for user" do
user = users(:david)
export = Account::SingleUserExport.create!(account: Current.account, user: user)
export.build
assert export.completed?
assert export.file.attached?
Tempfile.create([ "test", ".zip" ]) do |temp|
temp.binmode
export.file.download { |chunk| temp.write(chunk) }
temp.rewind
Zip::File.open(temp.path) do |zip|
json_files = zip.glob("*.json")
assert json_files.any?, "Zip should contain at least one JSON file"
json_content = JSON.parse(zip.read(json_files.first.name))
assert json_content.key?("number")
assert json_content.key?("title")
assert json_content.key?("board")
assert json_content.key?("creator")
assert json_content["creator"].key?("id")
assert json_content["creator"].key?("name")
assert json_content["creator"].key?("email")
assert json_content.key?("description")
assert json_content.key?("comments")
end
end
end
end
@@ -0,0 +1,7 @@
require "test_helper"
class Account::WholeAccountExportTest < ActiveSupport::TestCase
test "build generates zip with account data" do
skip "WholeAccountExport implementation incomplete"
end
end