Split Export model into user and whole-account export
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user