From e9115b10b5acf1847a25af1168c8c2824e2b7636 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 15 May 2023 11:41:38 +0200 Subject: [PATCH 1/5] =?UTF-8?q?correctif(data.kc):=20re-import=20les=20don?= =?UTF-8?q?n=C3=A9es=20kc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/recovery/exporter.rb | 21 ++++++++++++ app/lib/recovery/importer.rb | 17 ++++++++++ app/lib/recovery/life_cycle.rb | 33 ++++++++++++++++++ spec/fixtures/recovery/export.dump | Bin 0 -> 22069 bytes spec/lib/recovery/exporter_spec.rb | 48 +++++++++++++++++++++++++++ spec/lib/recovery/importer_spec.rb | 16 +++++++++ spec/lib/recovery/life_cycle_spec.rb | 8 +++++ 7 files changed, 143 insertions(+) create mode 100644 app/lib/recovery/exporter.rb create mode 100644 app/lib/recovery/importer.rb create mode 100644 app/lib/recovery/life_cycle.rb create mode 100644 spec/fixtures/recovery/export.dump create mode 100644 spec/lib/recovery/exporter_spec.rb create mode 100644 spec/lib/recovery/importer_spec.rb create mode 100644 spec/lib/recovery/life_cycle_spec.rb diff --git a/app/lib/recovery/exporter.rb b/app/lib/recovery/exporter.rb new file mode 100644 index 000000000..e397feb82 --- /dev/null +++ b/app/lib/recovery/exporter.rb @@ -0,0 +1,21 @@ +module Recovery + class Exporter + FILE_PATH = Rails.root.join('lib', 'data', 'export.dump') + + attr_reader :dossiers + def initialize(dossier_ids:, file_path: FILE_PATH) + dossier_with_data = Dossier.where(id: dossier_ids) + .preload(:user, + :individual, + :etablissement, + revision: :procedure) + @dossiers = DossierPreloader.new(dossier_with_data).all + @file_path = file_path + end + + def dump + File.open(@file_path, 'wb') { _1.write(Marshal.dump(@dossiers)) } + end + end +end + diff --git a/app/lib/recovery/importer.rb b/app/lib/recovery/importer.rb new file mode 100644 index 000000000..c2f2543b8 --- /dev/null +++ b/app/lib/recovery/importer.rb @@ -0,0 +1,17 @@ +module Recovery + class Importer + attr_reader :dossiers + + def initialize(file_path: Recovery::Exporter::FILE_PATH) + @dossiers = Marshal.load(File.read(file_path)) + end + + def load + @dossiers.map do |dossier| + dossier.instance_variable_set :@new_record, true + dossier.save! + end + end + end +end + diff --git a/app/lib/recovery/life_cycle.rb b/app/lib/recovery/life_cycle.rb new file mode 100644 index 000000000..ed2bf83e4 --- /dev/null +++ b/app/lib/recovery/life_cycle.rb @@ -0,0 +1,33 @@ +module Recovery + class LifeCycle + def initialize(dossier_ids:) + @dossier_ids = dossier_ids + end + + def load_export_destroy_and_import + export_dossiers + destroy_dossiers + import_dossiers + end + + def exporter + @exporter ||= Recovery::Exporter.new(dossier_ids: @dossier_ids) + end + + def importer + @importer ||= Importer.new() + end + + def export_dossiers + exporter.dump + end + + def destroy_dossiers + Dossier.where(id: @dossier_ids).destroy_all + end + + def import_dossiers + importer.load + end + end +end diff --git a/spec/fixtures/recovery/export.dump b/spec/fixtures/recovery/export.dump new file mode 100644 index 0000000000000000000000000000000000000000..983cf88b907156094a395add6a8662cfd159aade GIT binary patch literal 22069 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 bEL8BQVHc;FMM-p_0!9tHIL$1UJNEw Date: Mon, 15 May 2023 12:54:01 +0200 Subject: [PATCH 2/5] factories: add traits about files --- spec/factories/avis.rb | 8 ++++++++ spec/factories/dossier_operation_log.rb | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/spec/factories/avis.rb b/spec/factories/avis.rb index 0a16c7528..dde9a3b22 100644 --- a/spec/factories/avis.rb +++ b/spec/factories/avis.rb @@ -29,5 +29,13 @@ FactoryBot.define do trait :with_answer do answer { "Mon avis se décompose en deux points :\n- La demande semble pertinente\n- Le demandeur remplit les conditions." } end + + trait :with_introduction do + introduction_file { Rack::Test::UploadedFile.new('spec/fixtures/files/white.png', 'image/png') } + end + + trait :with_piece_justificative do + piece_justificative_file { Rack::Test::UploadedFile.new('spec/fixtures/files/white.png', 'image/png') } + end end end diff --git a/spec/factories/dossier_operation_log.rb b/spec/factories/dossier_operation_log.rb index ff6a80a53..191da8c27 100644 --- a/spec/factories/dossier_operation_log.rb +++ b/spec/factories/dossier_operation_log.rb @@ -1,5 +1,9 @@ FactoryBot.define do factory :dossier_operation_log do operation { :passer_en_instruction } + + trait :with_serialized do + serialized { Rack::Test::UploadedFile.new('spec/fixtures/files/white.png', 'image/png') } + end end end From 06b66636623a32e78c06c7c6eebad4e7023e4386 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 May 2023 12:54:20 +0200 Subject: [PATCH 3/5] import more stuff --- app/lib/recovery/exporter.rb | 11 ++- app/lib/recovery/importer.rb | 88 +++++++++++++++++++++- app/models/dossier_preloader.rb | 4 +- spec/lib/recovery/exporter_spec.rb | 6 +- spec/lib/recovery/importer_spec.rb | 4 +- spec/lib/recovery/life_cycle_spec.rb | 106 ++++++++++++++++++++++++++- 6 files changed, 205 insertions(+), 14 deletions(-) diff --git a/app/lib/recovery/exporter.rb b/app/lib/recovery/exporter.rb index e397feb82..0c5946b34 100644 --- a/app/lib/recovery/exporter.rb +++ b/app/lib/recovery/exporter.rb @@ -7,7 +7,15 @@ module Recovery dossier_with_data = Dossier.where(id: dossier_ids) .preload(:user, :individual, - :etablissement, + :invites, + :traitements, + :transfer_logs, + commentaires: { piece_jointe_attachment: :blob }, + avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob }, + dossier_operation_logs: { serialized_attachment: :blob }, + attestation: { pdf_attachment: :blob }, + justificatif_motivation_attachment: :blob, + etablissement: :exercices, revision: :procedure) @dossiers = DossierPreloader.new(dossier_with_data).all @file_path = file_path @@ -18,4 +26,3 @@ module Recovery end end end - diff --git a/app/lib/recovery/importer.rb b/app/lib/recovery/importer.rb index c2f2543b8..913641394 100644 --- a/app/lib/recovery/importer.rb +++ b/app/lib/recovery/importer.rb @@ -9,9 +9,93 @@ module Recovery def load @dossiers.map do |dossier| dossier.instance_variable_set :@new_record, true - dossier.save! + + Dossier.insert(dossier.attributes) + + Etablissement.insert(dossier.etablissement.attributes) + if dossier.etablissement.present? + APIEntreprise::EntrepriseJob.perform_later(dossier.etablissement.id, dossier.procedure.id) + end + + Individual.insert(dossier.individual.attributes) + + dossier.invites.each do |invite| + Invite.insert(invite.attributes) + end + + dossier.traitements.each do |traitement| + Traitement.insert(traitement.attributes) + end + + dossier.transfer_logs.each do |transfer| + DossierTransferLog.insert(transfer.attributes) + end + + dossier.etablissement.exercices.each do |exercice| + Exercice.insert(exercice.attributes) + end + + dossier.commentaires.each do |commentaire| + Commentaire.insert(commentaire.attributes) + if commentaire.piece_jointe.attached? + import(commentaire.piece_jointe) + end + end + + dossier.avis.each do |avis| + Avis.insert(avis.attributes) + + if avis.introduction_file.attached? + import(avis.introduction_file) + end + + if avis.piece_justificative_file.attached? + import(avis.piece_justificative_file) + end + end + + dossier.dossier_operation_logs.each do |dol| + DossierOperationLog.insert(dol.attributes) + + if dol.serialized.attached? + import(dol.serialized) + end + end + + if dossier.attestation.present? + Attestation.insert(dossier.attestation.attributes) + import(dossier.attestation.pdf) + end + + if dossier.justificatif_motivation.attached? + import(dossier.justificatif_motivation) + end + + dossier.champs.each do |champ| + champ.piece_justificative_file.each { |pj| import(pj) } + + if champ.etablissement.present? + APIEntreprise::EntrepriseJob.perform_later(champ.etablissement.id, dossier.procedure.id) + + champ.etablissement.exercices.each do |exercice| + Exercice.insert(exercice.attributes) + end + + Etablissement.insert(champ.etablissement.attributes) + end + + Champ.insert(champ.attributes) + + if champ.geo_areas.present? + champ.geo_areas.each { GeoArea.insert(_1.attributes) } + end + end end end + + def import(pj) + ActiveStorage::Blob.insert(pj.blob.attributes) + ActiveStorage::Attachment.insert(pj.attributes) + end end end - diff --git a/app/models/dossier_preloader.rb b/app/models/dossier_preloader.rb index 61e147492..69eedc82e 100644 --- a/app/models/dossier_preloader.rb +++ b/app/models/dossier_preloader.rb @@ -35,7 +35,7 @@ class DossierPreloader end def load_dossiers(dossiers, pj_template: false) - to_include = [piece_justificative_file_attachments: :blob] + to_include = [:geo_areas, piece_justificative_file_attachments: :blob, etablissement: :exercices] if pj_template to_include << { type_de_champ: { piece_justificative_template_attachment: :blob } } @@ -65,7 +65,7 @@ class DossierPreloader def load_etablissements(champs) champs_siret = champs.filter(&:siret?) - etablissements_by_id = Etablissement.where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id) + etablissements_by_id = Etablissement.includes(:exercices).where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id) champs_siret.each do |champ| etablissement = etablissements_by_id[champ.etablissement_id] champ.association(:etablissement).target = etablissement diff --git a/spec/lib/recovery/exporter_spec.rb b/spec/lib/recovery/exporter_spec.rb index d336f100d..d5a23b5e4 100644 --- a/spec/lib/recovery/exporter_spec.rb +++ b/spec/lib/recovery/exporter_spec.rb @@ -5,7 +5,7 @@ describe Recovery::Exporter do def cleanup_export_file # if File.exist?(fp) - # FileUtils.rm(fp) + # FileUtils.rm(fp) # end end @@ -13,11 +13,11 @@ describe Recovery::Exporter do after { cleanup_export_file } it 'exports dossiers to .dump' do - expect{ subject }.not_to raise_error + expect { subject }.not_to raise_error end it 'exports dossiers local file .dump' do - expect{ subject }.to change { File.exist?(fp) } + expect { subject }.to change { File.exist?(fp) } .from(false).to(true) end diff --git a/spec/lib/recovery/importer_spec.rb b/spec/lib/recovery/importer_spec.rb index e0579d341..7e5f3678d 100644 --- a/spec/lib/recovery/importer_spec.rb +++ b/spec/lib/recovery/importer_spec.rb @@ -3,7 +3,7 @@ describe Recovery::Importer do let(:importer) { Recovery::Importer.new(file_path:) } subject { importer.load } context 'loaded_data' do - let(:loaded_dossiers) { importer.dossiers} + let(:loaded_dossiers) { importer.dossiers } it 'contains user' do expect(loaded_dossiers.first.user).to be_an_instance_of(User) @@ -11,6 +11,6 @@ describe Recovery::Importer do end it 're-import dossiers from .dump' do - expect{ subject }.to change { Dossier.count }.by(importer.dossiers.size) + expect { subject }.to change { Dossier.count }.by(importer.dossiers.size) end end diff --git a/spec/lib/recovery/life_cycle_spec.rb b/spec/lib/recovery/life_cycle_spec.rb index 8dc2318a2..c5e752bc5 100644 --- a/spec/lib/recovery/life_cycle_spec.rb +++ b/spec/lib/recovery/life_cycle_spec.rb @@ -1,8 +1,108 @@ describe Recovery::LifeCycle do describe '.load_export_destroy_and_import' do - it 'works with one dossier' do - dossier = create(:dossier, :with_individual) - expect { Recovery::LifeCycle.new(dossier_ids: [dossier.id]).load_export_destroy_and_import }.not_to change {Dossier.count} + let(:procedure) do + create(:procedure, + types_de_champ_public: [ + { type: :repetition, children: [{ type: :piece_justificative }] }, + { type: :carte }, + { type: :siret } + ]) + end + + let(:some_file) { Rack::Test::UploadedFile.new('spec/fixtures/files/white.png', 'image/png') } + let(:geo_area) { build(:geo_area, :selection_utilisateur, :polygon) } + + let(:dossier) do + d = create(:dossier, procedure:) + + repetition(d).add_row(d.revision) + pj_champ(d).piece_justificative_file.attach(some_file) + carte(d).update(geo_areas: [geo_area]) + d.etablissement = create(:etablissement, :with_exercices) + d.etablissement.entreprise_attestation_sociale.attach(some_file) + d.etablissement.entreprise_attestation_fiscale.attach(some_file) + + siret(d).update(etablissement: create(:etablissement, :with_exercices)) + siret(d).etablissement.entreprise_attestation_sociale.attach(some_file) + siret(d).etablissement.entreprise_attestation_fiscale.attach(some_file) + + d.individual = build(:individual) + + d.attestation = build(:attestation, :with_pdf) + d.justificatif_motivation.attach(some_file) + + d.commentaires << build(:commentaire, :with_file) + + d.invites << build(:invite, :with_user) + + d.avis << build(:avis, :with_introduction, :with_piece_justificative) + + d.traitements.accepter(motivation: 'oui', processed_at: Time.zone.now) + d.save + + d.dossier_operation_logs << build(:dossier_operation_log, :with_serialized) + + d.transfer_logs.create(from: create(:user), to: create(:user)) + + d + end + + def repetition(d) = d.champs.find_by(type: "Champs::RepetitionChamp") + def pj_champ(d) = d.champs.find_by(type: "Champs::PieceJustificativeChamp") + def carte(d) = d.champs.find_by(type: "Champs::CarteChamp") + def siret(d) = d.champs.find_by(type: "Champs::SiretChamp") + + let(:instructeur) { create(:instructeur) } + + before do + instructeur.followed_dossiers << dossier + end + + it 'reloads the full grappe' do + expect(Dossier.count).to eq(1) + expect(Dossier.first.champs.count).not_to be(0) + + Recovery::LifeCycle.new(dossier_ids: [dossier.id]).load_export_destroy_and_import + + expect(Dossier.count).to eq(1) + + reloaded_dossier = Dossier.first + + expect(reloaded_dossier.champs.count).not_to be(0) + + expect(repetition(reloaded_dossier).champs.map(&:type)).to match_array(["Champs::PieceJustificativeChamp"]) + expect(pj_champ(reloaded_dossier).piece_justificative_file).to be_attached + expect(carte(reloaded_dossier).geo_areas).to be_present + + expect(reloaded_dossier.etablissement.exercices).to be_present + + # launch a job + # expect(reloaded_dossier.etablissement.entreprise_attestation_sociale).to be_attached + # expect(reloaded_dossier.etablissement.entreprise_attestation_fiscale).to be_attached + + expect(siret(reloaded_dossier).etablissement.exercices).to be_present + + # launch a job + # expect(siret(reloaded_dossier).etablissement.entreprise_attestation_sociale).to be_attached + # expect(siret(reloaded_dossier).etablissement.entreprise_attestation_fiscale).to be_attached + + expect(reloaded_dossier.individual).to be_present + expect(reloaded_dossier.attestation.pdf).to be_attached + expect(reloaded_dossier.justificatif_motivation).to be_attached + + expect(reloaded_dossier.commentaires.first.piece_jointe).to be_attached + + expect(reloaded_dossier.invites.first.user).to be_present + expect(reloaded_dossier.followers_instructeurs).to match_array([instructeur]) + + expect(reloaded_dossier.avis.first.introduction_file).to be_attached + expect(reloaded_dossier.avis.first.piece_justificative_file).to be_attached + + expect(reloaded_dossier.traitements).to be_present + + expect(reloaded_dossier.dossier_operation_logs.first.serialized).to be_attached + + expect(reloaded_dossier.transfer_logs).to be_present end end end From 9d1d523cf6316111435ba891465244697d63db9b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 May 2023 15:41:34 +0200 Subject: [PATCH 4/5] remove life_cycle --- app/lib/recovery/life_cycle.rb | 33 ---------------------------- spec/lib/recovery/life_cycle_spec.rb | 8 +++++-- 2 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 app/lib/recovery/life_cycle.rb diff --git a/app/lib/recovery/life_cycle.rb b/app/lib/recovery/life_cycle.rb deleted file mode 100644 index ed2bf83e4..000000000 --- a/app/lib/recovery/life_cycle.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Recovery - class LifeCycle - def initialize(dossier_ids:) - @dossier_ids = dossier_ids - end - - def load_export_destroy_and_import - export_dossiers - destroy_dossiers - import_dossiers - end - - def exporter - @exporter ||= Recovery::Exporter.new(dossier_ids: @dossier_ids) - end - - def importer - @importer ||= Importer.new() - end - - def export_dossiers - exporter.dump - end - - def destroy_dossiers - Dossier.where(id: @dossier_ids).destroy_all - end - - def import_dossiers - importer.load - end - end -end diff --git a/spec/lib/recovery/life_cycle_spec.rb b/spec/lib/recovery/life_cycle_spec.rb index c5e752bc5..4f71f7bd3 100644 --- a/spec/lib/recovery/life_cycle_spec.rb +++ b/spec/lib/recovery/life_cycle_spec.rb @@ -1,4 +1,4 @@ -describe Recovery::LifeCycle do +describe 'Recovery::LifeCycle' do describe '.load_export_destroy_and_import' do let(:procedure) do create(:procedure, @@ -62,7 +62,11 @@ describe Recovery::LifeCycle do expect(Dossier.count).to eq(1) expect(Dossier.first.champs.count).not_to be(0) - Recovery::LifeCycle.new(dossier_ids: [dossier.id]).load_export_destroy_and_import + @dossier_ids = Dossier.ids + + Recovery::Exporter.new(dossier_ids: @dossier_ids).dump + Dossier.where(id: @dossier_ids).destroy_all + Recovery::Importer.new().load expect(Dossier.count).to eq(1) From f76e52cc973b7cfc8c22658d91356b087122ec92 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 15 May 2023 17:41:53 +0200 Subject: [PATCH 5/5] tech(tache.recovery): ajoute une tache pour re-importer des dossiers venant d'un backup Update app/lib/recovery/exporter.rb Co-authored-by: Colin Darie --- app/lib/recovery/exporter.rb | 8 +++--- app/lib/recovery/importer.rb | 27 ++++++++++++------- app/models/dossier_preloader.rb | 10 ++++--- lib/tasks/recovery.rake | 38 +++++++++++++++++++++++++++ spec/fixtures/recovery/export.dump | Bin 22069 -> 0 bytes spec/lib/recovery/exporter_spec.rb | 33 +++-------------------- spec/lib/recovery/importer_spec.rb | 16 ----------- spec/lib/recovery/life_cycle_spec.rb | 13 ++++++--- 8 files changed, 82 insertions(+), 63 deletions(-) create mode 100644 lib/tasks/recovery.rake delete mode 100644 spec/fixtures/recovery/export.dump delete mode 100644 spec/lib/recovery/importer_spec.rb diff --git a/app/lib/recovery/exporter.rb b/app/lib/recovery/exporter.rb index 0c5946b34..4781896f1 100644 --- a/app/lib/recovery/exporter.rb +++ b/app/lib/recovery/exporter.rb @@ -2,7 +2,7 @@ module Recovery class Exporter FILE_PATH = Rails.root.join('lib', 'data', 'export.dump') - attr_reader :dossiers + attr_reader :dossiers, :file_path def initialize(dossier_ids:, file_path: FILE_PATH) dossier_with_data = Dossier.where(id: dossier_ids) .preload(:user, @@ -17,12 +17,14 @@ module Recovery justificatif_motivation_attachment: :blob, etablissement: :exercices, revision: :procedure) - @dossiers = DossierPreloader.new(dossier_with_data).all + @dossiers = DossierPreloader.new(dossier_with_data, + includes_for_dossier: [:geo_areas, etablissement: :exercices], + includes_for_etablissement: [:exercices]).all @file_path = file_path end def dump - File.open(@file_path, 'wb') { _1.write(Marshal.dump(@dossiers)) } + @file_path.binwrite(Marshal.dump(@dossiers)) end end end diff --git a/app/lib/recovery/importer.rb b/app/lib/recovery/importer.rb index 913641394..cb8f6500b 100644 --- a/app/lib/recovery/importer.rb +++ b/app/lib/recovery/importer.rb @@ -3,7 +3,9 @@ module Recovery attr_reader :dossiers def initialize(file_path: Recovery::Exporter::FILE_PATH) + # rubocop:disable Security/MarshalLoad @dossiers = Marshal.load(File.read(file_path)) + # rubocop:enable Security/MarshalLoad end def load @@ -12,12 +14,19 @@ module Recovery Dossier.insert(dossier.attributes) - Etablissement.insert(dossier.etablissement.attributes) if dossier.etablissement.present? - APIEntreprise::EntrepriseJob.perform_later(dossier.etablissement.id, dossier.procedure.id) - end + Etablissement.insert(dossier.etablissement.attributes) + if dossier.etablissement.present? + APIEntreprise::EntrepriseJob.perform_later(dossier.etablissement.id, dossier.procedure.id) + end - Individual.insert(dossier.individual.attributes) + dossier.etablissement.exercices.each do |exercice| + Exercice.insert(exercice.attributes) + end + end + if dossier.individual.present? + Individual.insert(dossier.individual.attributes) + end dossier.invites.each do |invite| Invite.insert(invite.attributes) @@ -31,10 +40,6 @@ module Recovery DossierTransferLog.insert(transfer.attributes) end - dossier.etablissement.exercices.each do |exercice| - Exercice.insert(exercice.attributes) - end - dossier.commentaires.each do |commentaire| Commentaire.insert(commentaire.attributes) if commentaire.piece_jointe.attached? @@ -53,8 +58,11 @@ module Recovery import(avis.piece_justificative_file) end end - dossier.dossier_operation_logs.each do |dol| + if dol.operation.nil? + puts "dol nil: #{dol.id}" + next + end DossierOperationLog.insert(dol.attributes) if dol.serialized.attached? @@ -90,6 +98,7 @@ module Recovery champ.geo_areas.each { GeoArea.insert(_1.attributes) } end end + puts "imported dossier: #{dossier.id}" end end diff --git a/app/models/dossier_preloader.rb b/app/models/dossier_preloader.rb index 69eedc82e..d17d67a49 100644 --- a/app/models/dossier_preloader.rb +++ b/app/models/dossier_preloader.rb @@ -1,8 +1,10 @@ class DossierPreloader DEFAULT_BATCH_SIZE = 2000 - def initialize(dossiers) + def initialize(dossiers, includes_for_dossier: [], includes_for_etablissement: []) @dossiers = dossiers + @includes_for_etablissement = includes_for_etablissement + @includes_for_dossier = includes_for_dossier end def in_batches(size = DEFAULT_BATCH_SIZE) @@ -35,7 +37,8 @@ class DossierPreloader end def load_dossiers(dossiers, pj_template: false) - to_include = [:geo_areas, piece_justificative_file_attachments: :blob, etablissement: :exercices] + to_include = @includes_for_dossier.dup + to_include << [piece_justificative_file_attachments: :blob] if pj_template to_include << { type_de_champ: { piece_justificative_template_attachment: :blob } } @@ -64,8 +67,9 @@ class DossierPreloader end def load_etablissements(champs) + to_include = @includes_for_etablissement.dup champs_siret = champs.filter(&:siret?) - etablissements_by_id = Etablissement.includes(:exercices).where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id) + etablissements_by_id = Etablissement.includes(to_include).where(id: champs_siret.map(&:etablissement_id).compact).index_by(&:id) champs_siret.each do |champ| etablissement = etablissements_by_id[champ.etablissement_id] champ.association(:etablissement).target = etablissement diff --git a/lib/tasks/recovery.rake b/lib/tasks/recovery.rake new file mode 100644 index 000000000..b5696d83c --- /dev/null +++ b/lib/tasks/recovery.rake @@ -0,0 +1,38 @@ +namespace :recovery do + desc <<~USAGE + given a file path, read it as json data, preload dossier data and export to marshal.dump. + the given file should be a json formatted as follow + { + procedure_id_1: [ + dossier_id_1, + dossier_id_2, + ... + ], + procedure_id_2: [ + ... + ], + ... + } + ex: rails recovery:export[missing_dossier_ids_per_procedure.json] + USAGE + task :export, [:file_path] => :environment do |_t, args| + dossier_ids = JSON.parse(File.read(args[:file_path])).values.flatten + rake_puts "Expecting to generate a dump with #{dossier_ids.size} dossiers" + exporter = Recovery::Exporter.new(dossier_ids:) + rake_puts "Found on db #{exporter.dossiers.size} dossiers" + exporter.dump + rake_puts "Export done, see: #{exporter.file_path}" + end + + desc <<~USAGE + given a file path, read it as marshal data + the given file should be the result of recover:export + ex: rails recovery:import[/absolute/path/to/lib/data/export.dump] + USAGE + task :import, [:file_path] => :environment do |_t, args| + importer = Recovery::Importer.new(file_path: args[:file_path]) + rake_puts "Expecting to load #{importer.dossiers.size} dossiers" + importer.load + rake_puts "Mise à jour terminée" + end +end diff --git a/spec/fixtures/recovery/export.dump b/spec/fixtures/recovery/export.dump deleted file mode 100644 index 983cf88b907156094a395add6a8662cfd159aade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22069 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 bEL8BQVHc;FMM-p_0!9tHIL$1UJNEw