From a194616772e0b2b9a70b82f28db9ad21dd847d1c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 8 Apr 2022 17:05:02 +0200 Subject: [PATCH 1/9] refactor(export): remove unnecessary to_sym --- app/models/export.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/export.rb b/app/models/export.rb index a62872c5b..476372bce 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -49,11 +49,11 @@ class Export < ApplicationRecord FORMATS_WITH_TIME_SPAN = [:xlsx, :ods, :csv].flat_map do |format| time_span_types.keys.map do |time_span_type| - { format: format.to_sym, time_span_type: time_span_type } + { format: format, time_span_type: time_span_type } end end FORMATS = [:xlsx, :ods, :csv].map do |format| - { format: format.to_sym } + { format: format } end def compute_async From cf8c084a59f2836d66a81cb34e5de61cef35a5b6 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 8 Apr 2022 17:07:54 +0200 Subject: [PATCH 2/9] refactor: extract download_and_zip in to a shared service --- app/services/downloadable_file_service.rb | 27 ++++++++++++++ app/services/procedure_archive_service.rb | 28 +------------- spec/jobs/archive_creation_job_spec.rb | 4 +- .../downloadable_file_service_spec.rb | 37 +++++++++++++++++++ .../procedure_archive_service_spec.rb | 31 ---------------- 5 files changed, 67 insertions(+), 60 deletions(-) create mode 100644 app/services/downloadable_file_service.rb create mode 100644 spec/services/downloadable_file_service_spec.rb diff --git a/app/services/downloadable_file_service.rb b/app/services/downloadable_file_service.rb new file mode 100644 index 000000000..97909578b --- /dev/null +++ b/app/services/downloadable_file_service.rb @@ -0,0 +1,27 @@ +class DownloadableFileService + ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' } + + def self.download_and_zip(procedure, attachments, filename, &block) + Dir.mktmpdir(nil, ARCHIVE_CREATION_DIR) do |tmp_dir| + export_dir = File.join(tmp_dir, filename) + zip_path = File.join(ARCHIVE_CREATION_DIR, "#{filename}.zip") + + begin + FileUtils.remove_entry_secure(export_dir) if Dir.exist?(export_dir) + Dir.mkdir(export_dir) + + download_manager = DownloadManager::ProcedureAttachmentsExport.new(procedure, attachments, export_dir) + download_manager.download_all + + Dir.chdir(tmp_dir) do + File.delete(zip_path) if File.exist?(zip_path) + system 'zip', '-0', '-r', zip_path, filename + end + yield(zip_path) + ensure + FileUtils.remove_entry_secure(export_dir) if Dir.exist?(export_dir) + File.delete(zip_path) if File.exist?(zip_path) + end + end + end +end diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb index 31f0786f6..6da543ac5 100644 --- a/app/services/procedure_archive_service.rb +++ b/app/services/procedure_archive_service.rb @@ -1,8 +1,6 @@ require 'tempfile' class ProcedureArchiveService - ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' } - def initialize(procedure) @procedure = procedure end @@ -27,9 +25,9 @@ class ProcedureArchiveService attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers) - download_and_zip(archive, attachments) do |zip_filepath| ArchiveUploader.new(procedure: @procedure, archive: archive, filepath: zip_filepath) .upload + DownloadableFileService.download_and_zip(@procedure, attachments, zip_root_folder(archive)) do |zip_filepath| end end @@ -45,30 +43,6 @@ class ProcedureArchiveService private - def download_and_zip(archive, attachments, &block) - Dir.mktmpdir(nil, ARCHIVE_CREATION_DIR) do |tmp_dir| - archive_dir = File.join(tmp_dir, zip_root_folder(archive)) - zip_path = File.join(ARCHIVE_CREATION_DIR, "#{zip_root_folder(archive)}.zip") - - begin - FileUtils.remove_entry_secure(archive_dir) if Dir.exist?(archive_dir) - Dir.mkdir(archive_dir) - - download_manager = DownloadManager::ProcedureAttachmentsExport.new(@procedure, attachments, archive_dir) - download_manager.download_all - - Dir.chdir(tmp_dir) do - File.delete(zip_path) if File.exist?(zip_path) - system 'zip', '-0', '-r', zip_path, zip_root_folder(archive) - end - yield(zip_path) - ensure - FileUtils.remove_entry_secure(archive_dir) if Dir.exist?(archive_dir) - File.delete(zip_path) if File.exist?(zip_path) - end - end - end - def zip_root_folder(archive) "procedure-#{@procedure.id}-#{archive.id}" end diff --git a/spec/jobs/archive_creation_job_spec.rb b/spec/jobs/archive_creation_job_spec.rb index 2f4f38425..985a0d1d8 100644 --- a/spec/jobs/archive_creation_job_spec.rb +++ b/spec/jobs/archive_creation_job_spec.rb @@ -11,7 +11,7 @@ describe ArchiveCreationJob, type: :job do before { expect(InstructeurMailer).not_to receive(:send_archive) } it 'does not send email and forward error for retry' do - allow_any_instance_of(ProcedureArchiveService).to receive(:download_and_zip).and_raise(StandardError, "kaboom") + allow(DownloadableFileService).to receive(:download_and_zip).and_raise(StandardError, "kaboom") expect { job.perform_now }.to raise_error(StandardError, "kaboom") expect(archive.reload.failed?).to eq(true) end @@ -20,7 +20,7 @@ describe ArchiveCreationJob, type: :job do context 'when it works' do let(:mailer) { double('mailer', deliver_later: true) } before do - allow_any_instance_of(ProcedureArchiveService).to receive(:download_and_zip).and_return(true) + allow(DownloadableFileService).to receive(:download_and_zip).and_return(true) expect(InstructeurMailer).to receive(:send_archive).and_return(mailer) end diff --git a/spec/services/downloadable_file_service_spec.rb b/spec/services/downloadable_file_service_spec.rb new file mode 100644 index 000000000..5e8c3c497 --- /dev/null +++ b/spec/services/downloadable_file_service_spec.rb @@ -0,0 +1,37 @@ +describe DownloadableFileService do + let(:procedure) { create(:procedure, :published) } + let(:service) { ProcedureArchiveService.new(procedure) } + + describe '#download_and_zip' do + let(:archive) { build(:archive, id: '3') } + let(:filename) { service.send(:zip_root_folder, archive) } + + it 'create a tmpdir while block is running' do + previous_dir_list = Dir.entries(DownloadableFileService::ARCHIVE_CREATION_DIR) + + DownloadableFileService.download_and_zip(procedure, [], filename) do |_zip_file| + new_dir_list = Dir.entries(DownloadableFileService::ARCHIVE_CREATION_DIR) + expect(previous_dir_list).not_to eq(new_dir_list) + end + end + + it 'cleans up its tmpdir after block execution' do + expect { DownloadableFileService.download_and_zip(procedure, [], filename) { |zip_file| } } + .not_to change { Dir.entries(DownloadableFileService::ARCHIVE_CREATION_DIR) } + end + + it 'creates a zip with zip utility' do + expected_zip_path = File.join(DownloadableFileService::ARCHIVE_CREATION_DIR, "#{service.send(:zip_root_folder, archive)}.zip") + expect(DownloadableFileService).to receive(:system).with('zip', '-0', '-r', expected_zip_path, an_instance_of(String)) + DownloadableFileService.download_and_zip(procedure, [], filename) { |zip_path| } + end + + it 'cleans up its generated zip' do + expected_zip_path = File.join(DownloadableFileService::ARCHIVE_CREATION_DIR, "#{service.send(:zip_root_folder, archive)}.zip") + DownloadableFileService.download_and_zip(procedure, [], filename) do |_zip_path| + expect(File.exist?(expected_zip_path)).to be_truthy + end + expect(File.exist?(expected_zip_path)).to be_falsey + end + end +end diff --git a/spec/services/procedure_archive_service_spec.rb b/spec/services/procedure_archive_service_spec.rb index 72c99a62a..73b7c7917 100644 --- a/spec/services/procedure_archive_service_spec.rb +++ b/spec/services/procedure_archive_service_spec.rb @@ -172,37 +172,6 @@ describe ProcedureArchiveService do end end - describe '#download_and_zip' do - let(:archive) { build(:archive, id: '3') } - it 'create a tmpdir while block is running' do - previous_dir_list = Dir.entries(ProcedureArchiveService::ARCHIVE_CREATION_DIR) - - service.send(:download_and_zip, archive, []) do |_zip_file| - new_dir_list = Dir.entries(ProcedureArchiveService::ARCHIVE_CREATION_DIR) - expect(previous_dir_list).not_to eq(new_dir_list) - end - end - - it 'cleans up its tmpdir after block execution' do - expect { service.send(:download_and_zip, archive, []) { |zip_file| } } - .not_to change { Dir.entries(ProcedureArchiveService::ARCHIVE_CREATION_DIR) } - end - - it 'creates a zip with zip utility' do - expected_zip_path = File.join(ProcedureArchiveService::ARCHIVE_CREATION_DIR, "#{service.send(:zip_root_folder, archive)}.zip") - expect(service).to receive(:system).with('zip', '-0', '-r', expected_zip_path, an_instance_of(String)) - service.send(:download_and_zip, archive, []) { |zip_path| } - end - - it 'cleans up its generated zip' do - expected_zip_path = File.join(ProcedureArchiveService::ARCHIVE_CREATION_DIR, "#{service.send(:zip_root_folder, archive)}.zip") - service.send(:download_and_zip, archive, []) do |_zip_path| - expect(File.exist?(expected_zip_path)).to be_truthy - end - expect(File.exist?(expected_zip_path)).to be_falsey - end - end - private def create_dossier_for_month(year, month) From 561b83781e2bcb27cd1d72eab50aaa394d863341 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 8 Apr 2022 17:09:22 +0200 Subject: [PATCH 3/9] refactor(archive): remove dependencie on archive from ArchiveUploader --- app/services/archive_uploader.rb | 14 +++++++++----- app/services/procedure_archive_service.rb | 4 ++-- spec/services/archive_uploader_spec.rb | 12 ++++++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/services/archive_uploader.rb b/app/services/archive_uploader.rb index c3dd7581f..fc644a65e 100644 --- a/app/services/archive_uploader.rb +++ b/app/services/archive_uploader.rb @@ -4,7 +4,7 @@ class ArchiveUploader # when file size is bigger, active storage expects the chunks + a manifest. MAX_FILE_SIZE_FOR_BACKEND_BEFORE_CHUNKING = ENV.fetch('ACTIVE_STORAGE_FILE_SIZE_THRESHOLD_BEFORE_CUSTOM_UPLOAD') { 4.gigabytes }.to_i - def upload + def upload(archive) uploaded_blob = create_and_upload_blob begin archive.file.purge if archive.file.attached? @@ -21,9 +21,13 @@ class ArchiveUploader ) end + def blob + create_and_upload_blob + end + private - attr_reader :procedure, :archive, :filepath + attr_reader :procedure, :filename, :filepath def create_and_upload_blob if active_storage_service_local? || File.size(filepath) < MAX_FILE_SIZE_FOR_BACKEND_BEFORE_CHUNKING @@ -62,7 +66,7 @@ class ArchiveUploader def blob_default_params(filepath) { key: namespaced_object_key, - filename: archive.filename(procedure), + filename: filename, content_type: 'application/zip', metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } } @@ -89,9 +93,9 @@ class ArchiveUploader system(ENV.fetch('ACTIVE_STORAGE_BIG_FILE_UPLOADER_WITH_ENCRYPTION_PATH').to_s, filepath, blob.key, exception: true) end - def initialize(procedure:, archive:, filepath:) + def initialize(procedure:, filename:, filepath:) @procedure = procedure - @archive = archive + @filename = filename @filepath = filepath end end diff --git a/app/services/procedure_archive_service.rb b/app/services/procedure_archive_service.rb index 6da543ac5..d0aa7efea 100644 --- a/app/services/procedure_archive_service.rb +++ b/app/services/procedure_archive_service.rb @@ -25,9 +25,9 @@ class ProcedureArchiveService attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers) - ArchiveUploader.new(procedure: @procedure, archive: archive, filepath: zip_filepath) - .upload DownloadableFileService.download_and_zip(@procedure, attachments, zip_root_folder(archive)) do |zip_filepath| + ArchiveUploader.new(procedure: @procedure, filename: archive.filename(@procedure), filepath: zip_filepath) + .upload(archive) end end diff --git a/spec/services/archive_uploader_spec.rb b/spec/services/archive_uploader_spec.rb index eb6a41f0b..c916d7e2b 100644 --- a/spec/services/archive_uploader_spec.rb +++ b/spec/services/archive_uploader_spec.rb @@ -4,18 +4,18 @@ describe ProcedureArchiveService do let(:file) { Tempfile.new } let(:fixture_blob) { ActiveStorage::Blob.create_before_direct_upload!(filename: File.basename(file.path), byte_size: file.size, checksum: 'osf') } - let(:uploader) { ArchiveUploader.new(procedure: procedure, archive: archive, filepath: file.path) } + let(:uploader) { ArchiveUploader.new(procedure: procedure, filename: archive.filename(procedure), filepath: file.path) } describe '.upload' do context 'when active storage service is local' do it 'uploads with upload_with_active_storage' do expect(uploader).to receive(:active_storage_service_local?).and_return(true) expect(uploader).to receive(:upload_with_active_storage).and_return(fixture_blob) - uploader.upload + uploader.upload(archive) end it 'link the created blob as an attachment to the current archive instance' do - expect { uploader.upload } + expect { uploader.upload(archive) } .to change { ActiveStorage::Attachment.where(name: 'file', record_type: 'Archive', record_id: archive.id).count }.by(1) end end @@ -31,7 +31,7 @@ describe ProcedureArchiveService do it 'uploads with upload_with_active_storage' do expect(uploader).to receive(:upload_with_active_storage).and_return(fixture_blob) - uploader.upload + uploader.upload(archive) end end @@ -40,12 +40,12 @@ describe ProcedureArchiveService do it 'uploads with upload_with_chunking_wrapper' do expect(uploader).to receive(:upload_with_chunking_wrapper).and_return(fixture_blob) - uploader.upload + uploader.upload(archive) end it 'link the created blob as an attachment to the current archive instance' do expect(uploader).to receive(:upload_with_chunking_wrapper).and_return(fixture_blob) - expect { uploader.upload } + expect { uploader.upload(archive) } .to change { ActiveStorage::Attachment.where(name: 'file', record_type: 'Archive', record_id: archive.id).count }.by(1) end end From 87e5e6faeb3fe8847123fd76170533a6049dfea9 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 8 Apr 2022 17:10:46 +0200 Subject: [PATCH 4/9] refactor(export): add procedure attr to export service --- app/services/procedure_export_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index 7381b32ea..7366f3f53 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -1,5 +1,5 @@ class ProcedureExportService - attr_reader :dossiers + attr_reader :procedure, :dossiers def initialize(procedure, dossiers) @procedure = procedure @@ -40,12 +40,12 @@ class ProcedureExportService end def champs_repetables_options - revision = @procedure.active_revision + revision = procedure.active_revision champs_by_stable_id = dossiers .flat_map { |dossier| (dossier.champs + dossier.champs_private).filter(&:repetition?) } .group_by(&:stable_id) - @procedure.types_de_champ_for_procedure_presentation.repetition + procedure.types_de_champ_for_procedure_presentation.repetition .map { |type_de_champ_repetition| [type_de_champ_repetition, type_de_champ_repetition.types_de_champ_for_revision(revision).to_a] } .filter { |(_, types_de_champ)| types_de_champ.present? } .map do |(type_de_champ_repetition, types_de_champ)| @@ -85,7 +85,7 @@ class ProcedureExportService end def spreadsheet_columns(format) - types_de_champ = @procedure.types_de_champ_for_procedure_presentation.not_repetition.to_a + types_de_champ = procedure.types_de_champ_for_procedure_presentation.not_repetition.to_a Proc.new do |instance| instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ) From 2832ea0286546706a245ad0c168bafcc4d222f8e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 8 Apr 2022 17:12:34 +0200 Subject: [PATCH 5/9] refactor(export): return blob from to_* methods --- app/models/export.rb | 32 ++----------- app/services/procedure_export_service.rb | 48 +++++++++++++++++-- .../services/procedure_export_service_spec.rb | 16 +++---- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/app/models/export.rb b/app/models/export.rb index 476372bce..988e78c1a 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -63,13 +63,7 @@ class Export < ApplicationRecord def compute load_snapshot! - file.attach( - io: io, - filename: filename, - content_type: content_type, - # We generate the exports ourselves, so they are safe - metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } - ) + file.attach(blob) end def since @@ -177,32 +171,16 @@ class Export < ApplicationRecord end end - def filename - procedure_identifier = procedure.path || "procedure-#{procedure.id}" - "dossiers_#{procedure_identifier}_#{statut}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}" - end - - def io + def blob service = ProcedureExportService.new(procedure, dossiers_for_export) case format.to_sym when :csv - StringIO.new(service.to_csv) + service.to_csv when :xlsx - StringIO.new(service.to_xlsx) + service.to_xlsx when :ods - StringIO.new(service.to_ods) - end - end - - def content_type - case format.to_sym - when :csv - 'text/csv' - when :xlsx - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - when :ods - 'application/vnd.oasis.opendocument.spreadsheet' + service.to_ods end end diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index 7366f3f53..a9b3fc280 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -8,25 +8,63 @@ class ProcedureExportService end def to_csv - SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv)) + io = StringIO.new(SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv))) + create_blob(io, :csv) end def to_xlsx # We recursively build multi page spreadsheet - @tables.reduce(nil) do |package, table| + io = @tables.reduce(nil) do |package, table| SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package) - end.to_stream.read + end.to_stream + create_blob(io, :xlsx) end def to_ods # We recursively build multi page spreadsheet - @tables.reduce(nil) do |spreadsheet, table| + io = StringIO.new(@tables.reduce(nil) do |spreadsheet, table| SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet) - end.bytes + end.bytes) + create_blob(io, :ods) + end end private + def create_blob(io, format) + ActiveStorage::Blob.create_and_upload!( + io: io, + filename: filename(format), + content_type: content_type(format), + identify: false, + # We generate the exports ourselves, so they are safe + metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + ) + end + + def base_filename + @base_filename ||= "dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}" + end + + def filename(format) + "#{base_filename}.#{format}" + end + + def procedure_identifier + procedure.path || "procedure-#{procedure.id}" + end + + def content_type(format) + case format + when :csv + 'text/csv' + when :xlsx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + when :ods + 'application/vnd.oasis.opendocument.spreadsheet' + end + end + def etablissements @etablissements ||= dossiers.flat_map do |dossier| [dossier.champs, dossier.champs_private] diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index c91dace09..1021691b6 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -4,11 +4,9 @@ describe ProcedureExportService do describe 'to_data' do let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } subject do - Tempfile.create do |f| - f << ProcedureExportService.new(procedure, procedure.dossiers).to_xlsx - f.rewind - SimpleXlsxReader.open(f.path) - end + ProcedureExportService.new(procedure, procedure.dossiers) + .to_xlsx + .open { |f| SimpleXlsxReader.open(f.path) } end let(:dossiers_sheet) { subject.sheets.first } @@ -178,11 +176,9 @@ describe ProcedureExportService do context 'as csv' do subject do - Tempfile.create do |f| - f << ProcedureExportService.new(procedure, procedure.dossiers).to_csv - f.rewind - CSV.read(f.path) - end + ProcedureExportService.new(procedure, procedure.dossiers) + .to_csv + .open { |f| CSV.read(f.path) } end let(:nominal_headers) do From d14e13230534d91aa5afdf2b92f9cbbf9cc3a9a1 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 8 Apr 2022 17:12:53 +0200 Subject: [PATCH 6/9] feat(export): add zip format support --- app/models/export.rb | 17 ++++++++++++++--- app/services/procedure_export_service.rb | 9 +++++++++ .../views/instructeurs/procedures/fr.yml | 1 + spec/models/export_spec.rb | 6 +++--- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/models/export.rb b/app/models/export.rb index 988e78c1a..e6ceb89d7 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -18,8 +18,9 @@ class Export < ApplicationRecord enum format: { csv: 'csv', ods: 'ods', - xlsx: 'xlsx' - } + xlsx: 'xlsx', + zip: 'zip' + }, _prefix: true enum time_span_type: { everything: 'everything', @@ -52,7 +53,7 @@ class Export < ApplicationRecord { format: format, time_span_type: time_span_type } end end - FORMATS = [:xlsx, :ods, :csv].map do |format| + FORMATS = [:xlsx, :ods, :csv, :zip].map do |format| { format: format } end @@ -98,6 +99,10 @@ class Export < ApplicationRecord format == self.class.formats.fetch(:csv) end + def zip? + format == self.class.formats.fetch(:zip) + end + def self.find_or_create_export(format, groupe_instructeurs, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) create_with(groupe_instructeurs: groupe_instructeurs, procedure_presentation: procedure_presentation, procedure_presentation_snapshot: procedure_presentation&.snapshot) .includes(:procedure_presentation) @@ -128,6 +133,10 @@ class Export < ApplicationRecord csv: { time_span_type: not_filtered.filter(&:csv?).index_by(&:time_span_type), statut: filtered.filter(&:csv?).index_by(&:statut) + }, + zip: { + time_span_type: {}, + statut: filtered.filter(&:zip?).index_by(&:statut) } } end @@ -181,6 +190,8 @@ class Export < ApplicationRecord service.to_xlsx when :ods service.to_ods + when :zip + service.to_zip end end diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index a9b3fc280..907426fd6 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -27,6 +27,13 @@ class ProcedureExportService end.bytes) create_blob(io, :ods) end + + def to_zip + attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers, true) + + DownloadableFileService.download_and_zip(procedure, attachments, base_filename) do |zip_filepath| + ArchiveUploader.new(procedure: procedure, filename: filename(:zip), filepath: zip_filepath).blob + end end private @@ -62,6 +69,8 @@ class ProcedureExportService 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' when :ods 'application/vnd.oasis.opendocument.spreadsheet' + when :zip + 'application/zip' end end diff --git a/config/locales/views/instructeurs/procedures/fr.yml b/config/locales/views/instructeurs/procedures/fr.yml index 9cf758507..b4139e687 100644 --- a/config/locales/views/instructeurs/procedures/fr.yml +++ b/config/locales/views/instructeurs/procedures/fr.yml @@ -5,6 +5,7 @@ fr: everything_csv_html: Demander un export au format .csv
(uniquement les dossiers, sans les champs répétables) everything_xlsx_html: Demander un export au format .xlsx everything_ods_html: Demander un export au format .ods + everything_zip_html: Demander un export au format .zip everything_short: Demander un export au format %{export_format} everything_pending_html: Un export au format %{export_format} est en train d’être généré
(demandé il y a %{export_time}) everything_ready_html: Télécharger l’export au format %{export_format}
(généré il y a %{export_time}) diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 2bfdec18a..3511895e5 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -48,9 +48,9 @@ RSpec.describe Export, type: :model do context 'when an export is made for one groupe instructeur' do let!(:export) { create(:export, groupe_instructeurs: [gi_1, gi_2]) } - it { expect(Export.find_for_groupe_instructeurs([gi_1.id], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} } }) } - it { expect(Export.find_for_groupe_instructeurs([gi_2.id, gi_1.id], nil)).to eq({ csv: { statut: {}, time_span_type: { 'everything' => export } }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} } }) } - it { expect(Export.find_for_groupe_instructeurs([gi_1.id, gi_2.id, gi_3.id], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} } }) } + it { expect(Export.find_for_groupe_instructeurs([gi_1.id], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} }, zip: { statut: {}, time_span_type: {} } }) } + it { expect(Export.find_for_groupe_instructeurs([gi_2.id, gi_1.id], nil)).to eq({ csv: { statut: {}, time_span_type: { 'everything' => export } }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} }, zip: { statut: {}, time_span_type: {} } }) } + it { expect(Export.find_for_groupe_instructeurs([gi_1.id, gi_2.id, gi_3.id], nil)).to eq({ csv: { statut: {}, time_span_type: {} }, xlsx: { statut: {}, time_span_type: {} }, ods: { statut: {}, time_span_type: {} }, zip: { statut: {}, time_span_type: {} } }) } end end end From caace89d98e83d461347f76a4d210407f071a7f2 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 12 Apr 2022 11:59:24 +0200 Subject: [PATCH 7/9] refactor(export): use predefined helpers --- app/models/export.rb | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/app/models/export.rb b/app/models/export.rb index e6ceb89d7..545935673 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -87,22 +87,6 @@ class Export < ApplicationRecord procedure_presentation_id.present? end - def xlsx? - format == self.class.formats.fetch(:xlsx) - end - - def ods? - format == self.class.formats.fetch(:ods) - end - - def csv? - format == self.class.formats.fetch(:csv) - end - - def zip? - format == self.class.formats.fetch(:zip) - end - def self.find_or_create_export(format, groupe_instructeurs, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) create_with(groupe_instructeurs: groupe_instructeurs, procedure_presentation: procedure_presentation, procedure_presentation_snapshot: procedure_presentation&.snapshot) .includes(:procedure_presentation) @@ -123,20 +107,20 @@ class Export < ApplicationRecord { xlsx: { - time_span_type: not_filtered.filter(&:xlsx?).index_by(&:time_span_type), - statut: filtered.filter(&:xlsx?).index_by(&:statut) + time_span_type: not_filtered.filter(&:format_xlsx?).index_by(&:time_span_type), + statut: filtered.filter(&:format_xlsx?).index_by(&:statut) }, ods: { - time_span_type: not_filtered.filter(&:ods?).index_by(&:time_span_type), - statut: filtered.filter(&:ods?).index_by(&:statut) + time_span_type: not_filtered.filter(&:format_ods?).index_by(&:time_span_type), + statut: filtered.filter(&:format_ods?).index_by(&:statut) }, csv: { - time_span_type: not_filtered.filter(&:csv?).index_by(&:time_span_type), - statut: filtered.filter(&:csv?).index_by(&:statut) + time_span_type: not_filtered.filter(&:format_csv?).index_by(&:time_span_type), + statut: filtered.filter(&:format_csv?).index_by(&:statut) }, zip: { time_span_type: {}, - statut: filtered.filter(&:zip?).index_by(&:statut) + statut: filtered.filter(&:format_zip?).index_by(&:statut) } } end From 38a6b2db6379e7a3c36fd7cda1e32b939b3c6bee Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 12 Apr 2022 19:02:59 +0200 Subject: [PATCH 8/9] fix(graphql): fix AddressTypeType --- app/graphql/types/address_type.rb | 8 ++++---- app/graphql/types/personne_morale_type.rb | 2 +- spec/graphql/dossier_spec.rb | 25 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/graphql/types/address_type.rb b/app/graphql/types/address_type.rb index 86a1d8756..60f3370f3 100644 --- a/app/graphql/types/address_type.rb +++ b/app/graphql/types/address_type.rb @@ -1,10 +1,10 @@ module Types class AddressType < Types::BaseObject class AddressTypeType < Types::BaseEnum - value(:housenumber, "numéro « à la plaque »", value: :housenumber) - value(:street, "position « à la voie », placé approximativement au centre de celle-ci", value: :street) - value(:municipality, "numéro « à la commune »", value: :municipality) - value(:locality, "lieu-dit", value: :locality) + value(:housenumber, "numéro « à la plaque »", value: "housenumber") + value(:street, "position « à la voie », placé approximativement au centre de celle-ci", value: "street") + value(:municipality, "numéro « à la commune »", value: "municipality") + value(:locality, "lieu-dit", value: "locality") end field :label, String, "libellé complet de l’adresse", null: false diff --git a/app/graphql/types/personne_morale_type.rb b/app/graphql/types/personne_morale_type.rb index c8176befa..2d2d98d2b 100644 --- a/app/graphql/types/personne_morale_type.rb +++ b/app/graphql/types/personne_morale_type.rb @@ -104,7 +104,7 @@ module Types def address { label: object.adresse, - type: :housenumber, + type: "housenumber", street_number: object.numero_voie, street_name: object.nom_voie, street_address: object.nom_voie.present? ? [object.numero_voie, object.type_voie, object.nom_voie].compact.join(' ') : nil, diff --git a/spec/graphql/dossier_spec.rb b/spec/graphql/dossier_spec.rb index 7ff4cc1c3..6aba97515 100644 --- a/spec/graphql/dossier_spec.rb +++ b/spec/graphql/dossier_spec.rb @@ -31,6 +31,26 @@ RSpec.describe Types::DossierType, type: :graphql do let(:dossier) { create(:dossier, :accepte, :with_populated_champs, procedure: procedure) } let(:query) { DOSSIER_WITH_CHAMPS_QUERY } let(:variables) { { number: dossier.id } } + let(:address) do + { + "type" => "housenumber", + "label" => "33 Rue Rébeval 75019 Paris", + "city_code" => "75119", + "city_name" => "Paris", + "postal_code" => "75019", + "region_code" => "11", + "region_name" => "Île-de-France", + "street_name" => "Rue Rébeval", + "street_number" => "33", + "street_address" => "33 Rue Rébeval", + "department_code" => "75", + "department_name" => "Paris" + } + end + + before do + dossier.champs.second.update(data: address) + end it { expect(data[:dossier][:champs][0][:__typename]).to eq "CommuneChamp" } it { expect(data[:dossier][:champs][1][:__typename]).to eq "AddressChamp" } @@ -86,7 +106,12 @@ RSpec.describe Types::DossierType, type: :graphql do code } fragment AddressFragment on Address { + type + label cityName + cityCode + streetName + streetNumber } GRAPHQL end From 433c01b1e6a951a92ce688ce0d26c7cd0cc3c259 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 12 Apr 2022 19:20:50 +0200 Subject: [PATCH 9/9] Revert "Merge pull request #7137 from betagouv/faster_pdf" This reverts commit 9da44bd913b8400c9c90de506f375607d67340d4, reversing changes made to ebac71796caf2ecd9ec76e42614e045838179b4a. --- Gemfile | 1 + Gemfile.lock | 4 ++ app/assets/images/header/logo-ds-wide.png | Bin 18553 -> 0 bytes app/lib/active_storage/downloadable_file.rb | 5 +- app/models/dossier.rb | 5 +- app/services/pieces_justificatives_service.rb | 51 +++++------------- app/views/dossiers/dossier_vide.pdf.prawn | 2 +- app/views/dossiers/show.pdf.prawn | 23 ++++---- config/env.example.optional | 2 +- config/initializers/images.rb | 2 +- .../pieces_justificatives_service_spec.rb | 2 +- 11 files changed, 37 insertions(+), 60 deletions(-) delete mode 100644 app/assets/images/header/logo-ds-wide.png diff --git a/Gemfile b/Gemfile index 24f8c36c9..e210be466 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,7 @@ gem 'openid_connect' gem 'pg' gem 'phonelib' gem 'prawn-rails' # PDF Generation +gem 'prawn-svg' gem 'premailer-rails' gem 'puma' # Use Puma as the app server gem 'pundit' diff --git a/Gemfile.lock b/Gemfile.lock index 0faaa9859..4485809aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -459,6 +459,9 @@ GEM prawn prawn-table rails (>= 3.1.0) + prawn-svg (0.31.0) + css_parser (~> 1.6) + prawn (>= 0.11.1, < 3) prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) premailer (1.14.2) @@ -837,6 +840,7 @@ DEPENDENCIES pg phonelib prawn-rails + prawn-svg premailer-rails pry-byebug puma diff --git a/app/assets/images/header/logo-ds-wide.png b/app/assets/images/header/logo-ds-wide.png deleted file mode 100644 index a0ac0f35319574a377870e6f6150bf2d21b2e8d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18553 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 { normal: Rails.root.join('lib/prawn/fonts/marianne/marianne-regular.ttf' ), bold: Rails.root.join('lib/prawn/fonts/marianne/marianne-bold.ttf' ), @@ -217,12 +212,12 @@ prawn_document(page_size: "A4") do |pdf| pdf.font 'marianne' pdf.pad_bottom(40) do - pdf.image DOSSIER_PDF_EXPORT_LOGO_SRC, width: 300, position: :center + pdf.svg IO.read(DOSSIER_PDF_EXPORT_LOGO_SRC), width: 300, position: :center end format_in_2_columns(pdf, 'Dossier Nº', @dossier.id.to_s) - format_in_2_columns(pdf, 'Démarche', @procedure.libelle) - format_in_2_columns(pdf, 'Organisme', @procedure.organisation_name) + format_in_2_columns(pdf, 'Démarche', @dossier.procedure.libelle) + format_in_2_columns(pdf, 'Organisme', @dossier.procedure.organisation_name) add_etat_dossier(pdf, @dossier) diff --git a/config/env.example.optional b/config/env.example.optional index 8cee6a933..51b05c16d 100644 --- a/config/env.example.optional +++ b/config/env.example.optional @@ -69,7 +69,7 @@ DS_ENV="staging" # PROCEDURE_DEFAULT_LOGO_SRC="republique-francaise-logo.svg" # Instance customization: PDF export logo ---> to be put in "app/assets/images" -# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.png" +# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg" # Instance customization: watermark for identity documents # WATERMARK_FILE="" diff --git a/config/initializers/images.rb b/config/initializers/images.rb index 161c9fc24..c488aa279 100644 --- a/config/initializers/images.rb +++ b/config/initializers/images.rb @@ -17,4 +17,4 @@ MAILER_FOOTER_LOGO_SRC = ENV.fetch("MAILER_FOOTER_LOGO_SRC", "mailer/instructeur PROCEDURE_DEFAULT_LOGO_SRC = ENV.fetch("PROCEDURE_DEFAULT_LOGO_SRC", "republique-francaise-logo.svg") # Logo in PDF export of a "Dossier" -DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.png") +DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.svg") diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index a262d6817..a7b715786 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -192,7 +192,7 @@ describe PiecesJustificativesService do describe '.generate_dossier_export' do let(:dossier) { create(:dossier) } - subject { PiecesJustificativesService.generate_dossier_export(Dossier.where(id: dossier.id)) } + subject { PiecesJustificativesService.generate_dossier_export(dossier) } it "doesn't update dossier" do expect { subject }.not_to change { dossier.updated_at }