[#2180] High-level PJ to champ PJ migration service
This commit is contained in:
parent
e24242e4b2
commit
7d316b8369
2 changed files with 263 additions and 0 deletions
|
@ -0,0 +1,93 @@
|
|||
class PieceJustificativeToChampPieceJointeMigrationService
|
||||
def initialize(**params)
|
||||
params.each do |key, value|
|
||||
instance_variable_set("@#{key}", value)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_correct_storage_configuration!
|
||||
storage_service.ensure_openstack_copy_possible!(PieceJustificativeUploader)
|
||||
end
|
||||
|
||||
def convert_procedure_pjs_to_champ_pjs(procedure)
|
||||
types_de_champ_pj = PiecesJustificativesService.types_pj_as_types_de_champ(procedure)
|
||||
populate_champs_pjs!(procedure, types_de_champ_pj)
|
||||
|
||||
# Only destroy the old types PJ once everything has been safely migrated to
|
||||
# champs PJs. Destroying the types PJ will cascade and destroy the PJs,
|
||||
# and delete the linked objects from remote storage. This means that no other
|
||||
# cleanup action is required.
|
||||
procedure.types_de_piece_justificative.destroy_all
|
||||
end
|
||||
|
||||
def storage_service
|
||||
@storage_service ||= CarrierwaveActiveStorageMigrationService.new
|
||||
end
|
||||
|
||||
def populate_champs_pjs!(procedure, types_de_champ_pj)
|
||||
procedure.types_de_champ += types_de_champ_pj
|
||||
|
||||
# Unscope to make sure all dossiers are migrated, even the soft-deleted ones
|
||||
procedure.dossiers.unscope(where: :hidden_at).find_each do |dossier|
|
||||
champs_pj = types_de_champ_pj.map(&:build_champ)
|
||||
dossier.champs += champs_pj
|
||||
|
||||
champs_pj.each do |champ|
|
||||
type_pj_id = champ.type_de_champ.old_pj&.fetch('stable_id', nil)
|
||||
pj = dossier.retrieve_last_piece_justificative_by_type(type_pj_id)
|
||||
if pj.present?
|
||||
convert_pj_to_champ!(pj, champ)
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# If anything goes wrong, we roll back the migration by destroying the newly created
|
||||
# types de champ, champs blobs and attachments.
|
||||
types_de_champ_pj.each do |type_champ|
|
||||
type_champ.champ.each { |c| c.piece_justificative_file.purge }
|
||||
type_champ.destroy
|
||||
end
|
||||
|
||||
# Reraise the exception to abort the migration.
|
||||
raise
|
||||
end
|
||||
|
||||
def convert_pj_to_champ!(pj, champ)
|
||||
blob = make_blob(pj)
|
||||
|
||||
# Upload the file before creating the attachment to make sure MIME type
|
||||
# identification doesn’t fail.
|
||||
storage_service.copy_from_carrierwave_to_active_storage!(pj.content.path, blob)
|
||||
attachment = storage_service.make_attachment(champ, 'piece_justificative_file', blob)
|
||||
|
||||
# By reloading, we force ActiveStorage to look at the attachment again, and see
|
||||
# that one exists now. We do this so that, if we need to roll back and destroy the champ,
|
||||
# the blob, the attachment and the actual file on OpenStack also get deleted.
|
||||
champ.reload
|
||||
rescue
|
||||
# Destroy partially attached object that the more general rescue in `populate_champs_pjs!`
|
||||
# might not be able to handle.
|
||||
|
||||
if blob&.key.present?
|
||||
begin
|
||||
storage_service.delete_from_active_storage!(blob)
|
||||
rescue => e
|
||||
# The cleanup attempt failed, perhaps because the object had not been
|
||||
# successfully copied to the Active Storage bucket yet.
|
||||
# Continue trying to clean up the rest anyway.
|
||||
pp e
|
||||
end
|
||||
end
|
||||
|
||||
blob&.destroy
|
||||
attachment&.destroy
|
||||
champ.reload
|
||||
|
||||
# Reraise the exception to abort the migration.
|
||||
raise
|
||||
end
|
||||
|
||||
def make_blob(pj)
|
||||
storage_service.make_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,170 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PieceJustificativeToChampPieceJointeMigrationService do
|
||||
let(:service) { PieceJustificativeToChampPieceJointeMigrationService.new(storage_service: storage_service) }
|
||||
let(:storage_service) { CarrierwaveActiveStorageMigrationService.new }
|
||||
let(:pj_uploader) { class_double(PieceJustificativeUploader) }
|
||||
let(:pj_service) { class_double(PiecesJustificativesService) }
|
||||
|
||||
let(:procedure) { create(:procedure, types_de_piece_justificative: types_pj) }
|
||||
let(:types_pj) { [create(:type_de_piece_justificative)] }
|
||||
|
||||
let!(:dossier) do
|
||||
create(
|
||||
:dossier,
|
||||
procedure: procedure,
|
||||
pieces_justificatives: pjs
|
||||
)
|
||||
end
|
||||
|
||||
let(:pjs) { [] }
|
||||
|
||||
def make_pjs
|
||||
types_pj.map do |tpj|
|
||||
create(:piece_justificative, :contrat, type_de_piece_justificative: tpj)
|
||||
end
|
||||
end
|
||||
|
||||
def expect_storage_service_to_convert_object
|
||||
expect(storage_service).to receive(:make_blob)
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||
expect(storage_service).to receive(:make_attachment)
|
||||
end
|
||||
|
||||
context 'when conversion succeeds' do
|
||||
context 'for the procedure' do
|
||||
it 'types de champ are created for the "pièces jointes" header and for each PJ' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { procedure.types_de_champ.count }
|
||||
.by(types_pj.count + 1)
|
||||
end
|
||||
|
||||
it 'the old types de pj are removed' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { procedure.types_de_piece_justificative.count }
|
||||
.to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for the dossier' do
|
||||
let(:pjs) { make_pjs }
|
||||
|
||||
before { expect_storage_service_to_convert_object }
|
||||
|
||||
it 'champs are created for the "pièces jointes" header and for each PJ' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.champs.count }
|
||||
.by(types_pj.count + 1)
|
||||
end
|
||||
|
||||
it 'the old pjs are removed' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.pieces_justificatives.count }
|
||||
.to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dossier is soft-deleted it still gets converted' do
|
||||
let(:pjs) { make_pjs }
|
||||
|
||||
let!(:dossier) do
|
||||
create(
|
||||
:dossier,
|
||||
procedure: procedure,
|
||||
pieces_justificatives: pjs,
|
||||
hidden_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
|
||||
before { expect_storage_service_to_convert_object }
|
||||
|
||||
it 'champs are created for the "pièces jointes" header and for each PJ' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.champs.count }
|
||||
.by(types_pj.count + 1)
|
||||
end
|
||||
|
||||
it 'the old pjs are removed' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.pieces_justificatives.count }
|
||||
.to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are several pjs for one type' do
|
||||
let(:pjs) { make_pjs + make_pjs }
|
||||
|
||||
it 'only converts the most recent PJ for each type PJ' do
|
||||
expect(storage_service).to receive(:make_blob).exactly(types_pj.count)
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!).exactly(types_pj.count)
|
||||
expect(storage_service).to receive(:make_attachment).exactly(types_pj.count)
|
||||
|
||||
service.convert_procedure_pjs_to_champ_pjs(procedure)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'cleanup when conversion fails' do
|
||||
let(:pjs) { make_pjs }
|
||||
|
||||
let!(:failing_dossier) do
|
||||
create(
|
||||
:dossier,
|
||||
procedure: procedure,
|
||||
pieces_justificatives: make_pjs
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(storage_service).to receive(:checksum).and_return('cafe')
|
||||
allow(storage_service).to receive(:fix_content_type)
|
||||
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||
.and_raise('LOL no!')
|
||||
|
||||
expect(storage_service).to receive(:delete_from_active_storage!)
|
||||
end
|
||||
|
||||
def try_convert(procedure)
|
||||
service.convert_procedure_pjs_to_champ_pjs(procedure)
|
||||
rescue => e
|
||||
e
|
||||
end
|
||||
|
||||
it 'passes on the exception' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to raise_error('LOL no!')
|
||||
end
|
||||
|
||||
it 'does not create champs' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { dossier.champs.count }
|
||||
end
|
||||
|
||||
it 'does not remove any old pjs' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { dossier.pieces_justificatives.count }
|
||||
end
|
||||
|
||||
it 'does not creates types de champ' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { procedure.types_de_champ.count }
|
||||
end
|
||||
|
||||
it 'does not remove old types de pj' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { procedure.types_de_piece_justificative.count }
|
||||
end
|
||||
|
||||
it 'does not leave stale blobs behind' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { ActiveStorage::Blob.count }
|
||||
end
|
||||
|
||||
it 'does not leave stale attachments behind' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { ActiveStorage::Attachment.count }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue