diff --git a/app/controllers/api/v2/dossiers_controller.rb b/app/controllers/api/v2/dossiers_controller.rb index 0612aaf53..3c627bd36 100644 --- a/app/controllers/api/v2/dossiers_controller.rb +++ b/app/controllers/api/v2/dossiers_controller.rb @@ -2,7 +2,7 @@ class API::V2::DossiersController < API::V2::BaseController before_action :ensure_dossier_present def pdf - @acls = PiecesJustificativesService.new(user_profile: Administrateur.new).acl_for_dossier_export(dossier.procedure) + @acls = PiecesJustificativesService.new(user_profile: Administrateur.new, export_template: nil).acl_for_dossier_export(dossier.procedure) render(template: 'dossiers/show', formats: [:pdf]) end diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 04085f7c2..7ab9a717d 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -45,7 +45,7 @@ module Instructeurs @is_dossier_in_batch_operation = dossier.batch_operation.present? respond_to do |format| format.pdf do - @acls = PiecesJustificativesService.new(user_profile: current_instructeur).acl_for_dossier_export(dossier.procedure) + @acls = PiecesJustificativesService.new(user_profile: current_instructeur, export_template: nil).acl_for_dossier_export(dossier.procedure) render(template: 'dossiers/show', formats: [:pdf]) end format.all diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 8caf08fee..1559dca8c 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -324,13 +324,18 @@ module Instructeurs end def export_format - @export_format ||= params[:export_format] + @export_format ||= params[:export_format].presence || export_template&.kind + end + + def export_template + @export_template ||= ExportTemplate.find(params[:export_template_id]) if params[:export_template_id].present? end def export_options @export_options ||= { time_span_type: params[:time_span_type], statut: params[:statut], + export_template:, procedure_presentation: params[:statut].present? ? procedure_presentation : nil }.compact end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 6176ce7f3..db49503c7 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -88,7 +88,7 @@ module Users end def show - pj_service = PiecesJustificativesService.new(user_profile: current_user) + pj_service = PiecesJustificativesService.new(user_profile: current_user, export_template: nil) respond_to do |format| format.pdf do @dossier = dossier_with_champs(pj_template: false) diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb index b89c20997..57e919ff3 100644 --- a/app/lib/active_storage/downloadable_file.rb +++ b/app/lib/active_storage/downloadable_file.rb @@ -1,8 +1,8 @@ require 'fog/openstack' class ActiveStorage::DownloadableFile - def self.create_list_from_dossiers(dossiers:, user_profile:) - pj_service = PiecesJustificativesService.new(user_profile:) + def self.create_list_from_dossiers(dossiers:, user_profile:, export_template: nil) + pj_service = PiecesJustificativesService.new(user_profile:, export_template:) pj_service.generate_dossiers_export(dossiers) + pj_service.liste_documents(dossiers) end diff --git a/app/lib/download_manager/parallel_download_queue.rb b/app/lib/download_manager/parallel_download_queue.rb index 16dcbd762..35a6b8695 100644 --- a/app/lib/download_manager/parallel_download_queue.rb +++ b/app/lib/download_manager/parallel_download_queue.rb @@ -12,6 +12,8 @@ module DownloadManager end def download_all + # TODO: arriver à enelver ce parametrage d'ActiveStorage + ActiveStorage::Current.url_options = { host: ENV.fetch("APP_HOST") } hydra = Typhoeus::Hydra.new(max_concurrency: DOWNLOAD_MAX_PARALLEL) attachments.each do |attachment, path| diff --git a/app/models/champ.rb b/app/models/champ.rb index 2cb0bb4ec..6d3f02f11 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -91,6 +91,14 @@ class Champ < ApplicationRecord parent_id.present? end + def stable_id_with_row + [row_id, stable_id].compact + end + + def row_index + Champ.where(parent:).pluck(:row_id).sort.index(:id) + end + # used for the `required` html attribute # check visibility to avoid hidden required input # which prevent the form from being sent. diff --git a/app/models/export.rb b/app/models/export.rb index 66832d5e1..d9b29d409 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -67,9 +67,10 @@ class Export < ApplicationRecord procedure_presentation_id.present? end - def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) + def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil) attributes = { format:, + export_template:, time_span_type:, statut:, key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation) @@ -148,7 +149,7 @@ class Export < ApplicationRecord end def blob - service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile) + service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile, export_template) case format.to_sym when :csv diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 2fece0c0b..22040d4e0 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -1,6 +1,7 @@ class PiecesJustificativesService - def initialize(user_profile:) + def initialize(user_profile:, export_template:) @user_profile = user_profile + @export_template = export_template end def liste_documents(dossiers) @@ -58,7 +59,11 @@ class PiecesJustificativesService created_at: dossier.updated_at ) - pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a) + if @export_template + pdfs << @export_template.attachment_and_path(dossier, a) + else + pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a) + end end pdfs @@ -153,9 +158,14 @@ class PiecesJustificativesService .includes(:blob) .where(record_type: "Champ", record_id: champ_id_dossier_id.keys) .filter { |a| safe_attachment(a) } - .map do |a| + .map do |a, _i| dossier_id = champ_id_dossier_id[a.record_id] - ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) + pj_index = Champ.find(a.record_id).piece_justificative_file.blobs.map(&:id).index(a.blob_id) + if @export_template + @export_template.attachment_and_path(Dossier.find(dossier_id), a, index: pj_index, row_index: a.record.row_index) + else + ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) + end end end diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index 75eab89e8..5503a8a14 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -1,10 +1,11 @@ class ProcedureExportService attr_reader :procedure, :dossiers - def initialize(procedure, dossiers, user_profile) + def initialize(procedure, dossiers, user_profile, export_template) @procedure = procedure @dossiers = dossiers @user_profile = user_profile + @export_template = export_template end def to_csv @@ -36,7 +37,7 @@ class ProcedureExportService end def to_zip - attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers:, user_profile: @user_profile) + attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers:, user_profile: @user_profile, export_template: @export_template) DownloadableFileService.download_and_zip(procedure, attachments, base_filename) do |zip_filepath| ArchiveUploader.new(procedure: procedure, filename: filename(:zip), filepath: zip_filepath).blob diff --git a/spec/controllers/experts/avis_controller_spec.rb b/spec/controllers/experts/avis_controller_spec.rb index 7a4029eed..e58b4d2bb 100644 --- a/spec/controllers/experts/avis_controller_spec.rb +++ b/spec/controllers/experts/avis_controller_spec.rb @@ -121,7 +121,7 @@ describe Experts::AvisController, type: :controller do context 'with a valid avis' do it do service = instance_double(PiecesJustificativesService) - expect(PiecesJustificativesService).to receive(:new).with(user_profile: expert).and_return(service) + expect(PiecesJustificativesService).to receive(:new).with(user_profile: expert, export_template: nil).and_return(service) expect(service).to receive(:generate_dossiers_export).with(Dossier.where(id: dossier)).and_return([]) expect(service).to receive(:liste_documents).with(Dossier.where(id: dossier)).and_return([]) is_expected.to have_http_status(:success) diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 8b99ff44f..8add60a04 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -936,7 +936,7 @@ describe Instructeurs::DossiersController, type: :controller do subject end - it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: instructeur).acl_for_dossier_export(dossier.procedure)) } + it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: instructeur, export_template: nil).acl_for_dossier_export(dossier.procedure)) } it { expect(assigns(:is_dossier_in_batch_operation)).to eq(false) } it { expect(response).to render_template 'dossiers/show' } diff --git a/spec/controllers/instructeurs/procedures_controller_spec.rb b/spec/controllers/instructeurs/procedures_controller_spec.rb index ca0e1a4a9..3f34c3e53 100644 --- a/spec/controllers/instructeurs/procedures_controller_spec.rb +++ b/spec/controllers/instructeurs/procedures_controller_spec.rb @@ -736,6 +736,18 @@ describe Instructeurs::ProceduresController, type: :controller do end it { expect { subject }.to change { Export.where(user_profile: instructeur).count }.by(1) } + + context 'with an export template' do + let(:export_template) { create(:export_template) } + subject do + get :download_export, params: { export_template_id: export_template.id, procedure_id: procedure.id } + end + + it 'displays an notice' do + is_expected.to redirect_to(exports_instructeur_procedure_url(procedure)) + expect(flash.notice).to be_present + end + end end context 'when the export is not ready' do diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index e0fd3501e..fb1a28e53 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -1142,7 +1142,7 @@ describe Users::DossiersController, type: :controller do end context 'when the dossier has been submitted' do - it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: user).acl_for_dossier_export(dossier.procedure)) } + it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: user, export_template: nil).acl_for_dossier_export(dossier.procedure)) } it { expect(response).to render_template('dossiers/show') } end end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 5e7e1eda3..5b2a8ae99 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -109,6 +109,14 @@ RSpec.describe Export, type: :model do end end + context 'with export template' do + let(:export_template) { build(:export_template) } + it 'creates new export' do + expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, export_template: export_template, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) } + .to change { Export.count }.by(1) + end + end + context 'with existing matching export' do def find_or_create = Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 7a212912e..771cb8e96 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -2,8 +2,9 @@ describe PiecesJustificativesService do describe '.liste_documents' do let(:dossier) { create(:dossier, procedure: procedure) } let(:dossiers) { Dossier.where(id: dossier.id) } + let(:export_template) { nil } subject do - PiecesJustificativesService.new(user_profile:).liste_documents(dossiers).map(&:first) + PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:first) end context 'no acl' do @@ -19,6 +20,11 @@ describe PiecesJustificativesService do end it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + + context 'with export_template' do + let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } + it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + end end context 'with a multiple attachments' do @@ -303,7 +309,7 @@ describe PiecesJustificativesService do let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :piece_justificative }] }]) } let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) } let(:dossiers) { Dossier.where(id: dossier.id) } - subject { PiecesJustificativesService.new(user_profile:).generate_dossiers_export(dossiers) } + subject { PiecesJustificativesService.new(user_profile:, export_template: nil).generate_dossiers_export(dossiers) } it "doesn't update dossier" do expect { subject }.not_to change { dossier.updated_at } @@ -315,7 +321,7 @@ describe PiecesJustificativesService do let!(:not_confidentiel_avis) { create(:avis, :not_confidentiel, dossier: dossier) } let!(:expert_avis) { create(:avis, :confidentiel, dossier: dossier, expert: user_profile) } - subject { PiecesJustificativesService.new(user_profile:).generate_dossiers_export(dossiers) } + subject { PiecesJustificativesService.new(user_profile:, export_template: nil).generate_dossiers_export(dossiers) } it "includes avis not confidentiel as well as expert's avis" do expect_any_instance_of(Dossier).to receive(:avis_for_expert).with(user_profile).and_return([]) subject diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index aa2fb3733..9109124f4 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -2,8 +2,9 @@ require 'csv' describe ProcedureExportService do let(:instructeur) { create(:instructeur) } - let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs, instructeurs: [instructeur]) } - let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur) } + let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } + let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } + let(:export_template) { nil } describe 'to_xlsx' do subject do @@ -243,7 +244,7 @@ describe ProcedureExportService do context 'as csv' do subject do - ProcedureExportService.new(procedure, procedure.dossiers, instructeur) + ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) .to_csv .open { |f| CSV.read(f.path) } end @@ -519,39 +520,68 @@ describe ProcedureExportService do end end - context 'generate_dossiers_export' do + describe 'generate_dossiers_export' do it 'include_infos_administration (so it includes avis, champs privés)' do - expect(ActiveStorage::DownloadableFile).to receive(:create_list_from_dossiers).with(dossiers: anything, user_profile: instructeur).and_return([]) + expect(ActiveStorage::DownloadableFile).to receive(:create_list_from_dossiers).with(dossiers: anything, user_profile: instructeur, export_template:).and_return([]) subject end - end - context 'with files (and http calls)' do - let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } - let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur).generate_dossiers_export(Dossier.where(id: dossier)) } - before do - allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + context 'with export_template' do + let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } + let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) } + let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } + before do + allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + end + + it 'returns a blob with custom filenames' do + VCR.use_cassette('archive/new_file_to_get_200') do + subject + File.write('tmp.zip', subject.download, mode: 'wb') + File.open('tmp.zip') do |fd| + files = ZipTricks::FileReader.read_zip_structure(io: fd) + base_fn = "export" + structure = [ + "#{base_fn}/", + "#{base_fn}/dossier-#{dossier.id}/", + "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}.txt", + "#{base_fn}/dossier-#{dossier.id}/export_#{dossier.id}.pdf" + ] + expect(files.size).to eq(structure.size) + expect(files.map(&:filename)).to match_array(structure) + end + FileUtils.remove_entry_secure('tmp.zip') + end + end end - it 'returns a blob with valid files' do - VCR.use_cassette('archive/new_file_to_get_200') do - subject + context 'with files (and http calls)' do + let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } + let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template: nil).generate_dossiers_export(Dossier.where(id: dossier)) } + before do + allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + end - File.write('tmp.zip', subject.download, mode: 'wb') - File.open('tmp.zip') do |fd| - files = ZipTricks::FileReader.read_zip_structure(io: fd) - base_fn = 'export' - structure = [ - "#{base_fn}/", - "#{base_fn}/dossier-#{dossier.id}/", - "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", - "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", - "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" - ] - expect(files.size).to eq(structure.size) - expect(files.map(&:filename)).to match_array(structure) + it 'returns a blob with valid files' do + VCR.use_cassette('archive/new_file_to_get_200') do + subject + + File.write('tmp.zip', subject.download, mode: 'wb') + File.open('tmp.zip') do |fd| + files = ZipTricks::FileReader.read_zip_structure(io: fd) + base_fn = 'export' + structure = [ + "#{base_fn}/", + "#{base_fn}/dossier-#{dossier.id}/", + "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", + "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", + "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" + ] + expect(files.size).to eq(structure.size) + expect(files.map(&:filename)).to match_array(structure) + end + FileUtils.remove_entry_secure('tmp.zip') end - FileUtils.remove_entry_secure('tmp.zip') end end end