generate export with export_template

This commit is contained in:
Christophe Robillard 2024-03-08 17:14:21 +01:00 committed by simon lehericey
parent 7a39752630
commit 357c07456c
No known key found for this signature in database
GPG key ID: CDE670D827C7B3C5
17 changed files with 131 additions and 48 deletions

View file

@ -2,7 +2,7 @@ class API::V2::DossiersController < API::V2::BaseController
before_action :ensure_dossier_present before_action :ensure_dossier_present
def pdf 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]) render(template: 'dossiers/show', formats: [:pdf])
end end

View file

@ -45,7 +45,7 @@ module Instructeurs
@is_dossier_in_batch_operation = dossier.batch_operation.present? @is_dossier_in_batch_operation = dossier.batch_operation.present?
respond_to do |format| respond_to do |format|
format.pdf do 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]) render(template: 'dossiers/show', formats: [:pdf])
end end
format.all format.all

View file

@ -324,13 +324,18 @@ module Instructeurs
end end
def export_format 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 end
def export_options def export_options
@export_options ||= { @export_options ||= {
time_span_type: params[:time_span_type], time_span_type: params[:time_span_type],
statut: params[:statut], statut: params[:statut],
export_template:,
procedure_presentation: params[:statut].present? ? procedure_presentation : nil procedure_presentation: params[:statut].present? ? procedure_presentation : nil
}.compact }.compact
end end

View file

@ -88,7 +88,7 @@ module Users
end end
def show 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| respond_to do |format|
format.pdf do format.pdf do
@dossier = dossier_with_champs(pj_template: false) @dossier = dossier_with_champs(pj_template: false)

View file

@ -1,8 +1,8 @@
require 'fog/openstack' require 'fog/openstack'
class ActiveStorage::DownloadableFile class ActiveStorage::DownloadableFile
def self.create_list_from_dossiers(dossiers:, user_profile:) def self.create_list_from_dossiers(dossiers:, user_profile:, export_template: nil)
pj_service = PiecesJustificativesService.new(user_profile:) pj_service = PiecesJustificativesService.new(user_profile:, export_template:)
pj_service.generate_dossiers_export(dossiers) + pj_service.liste_documents(dossiers) pj_service.generate_dossiers_export(dossiers) + pj_service.liste_documents(dossiers)
end end

View file

@ -12,6 +12,8 @@ module DownloadManager
end end
def download_all 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) hydra = Typhoeus::Hydra.new(max_concurrency: DOWNLOAD_MAX_PARALLEL)
attachments.each do |attachment, path| attachments.each do |attachment, path|

View file

@ -91,6 +91,14 @@ class Champ < ApplicationRecord
parent_id.present? parent_id.present?
end 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 # used for the `required` html attribute
# check visibility to avoid hidden required input # check visibility to avoid hidden required input
# which prevent the form from being sent. # which prevent the form from being sent.

View file

@ -67,9 +67,10 @@ class Export < ApplicationRecord
procedure_presentation_id.present? procedure_presentation_id.present?
end 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 = { attributes = {
format:, format:,
export_template:,
time_span_type:, time_span_type:,
statut:, statut:,
key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation) key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation)
@ -148,7 +149,7 @@ class Export < ApplicationRecord
end end
def blob 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 case format.to_sym
when :csv when :csv

View file

@ -1,6 +1,7 @@
class PiecesJustificativesService class PiecesJustificativesService
def initialize(user_profile:) def initialize(user_profile:, export_template:)
@user_profile = user_profile @user_profile = user_profile
@export_template = export_template
end end
def liste_documents(dossiers) def liste_documents(dossiers)
@ -58,7 +59,11 @@ class PiecesJustificativesService
created_at: dossier.updated_at 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 end
pdfs pdfs
@ -153,9 +158,14 @@ class PiecesJustificativesService
.includes(:blob) .includes(:blob)
.where(record_type: "Champ", record_id: champ_id_dossier_id.keys) .where(record_type: "Champ", record_id: champ_id_dossier_id.keys)
.filter { |a| safe_attachment(a) } .filter { |a| safe_attachment(a) }
.map do |a| .map do |a, _i|
dossier_id = champ_id_dossier_id[a.record_id] 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
end end

View file

@ -1,10 +1,11 @@
class ProcedureExportService class ProcedureExportService
attr_reader :procedure, :dossiers attr_reader :procedure, :dossiers
def initialize(procedure, dossiers, user_profile) def initialize(procedure, dossiers, user_profile, export_template)
@procedure = procedure @procedure = procedure
@dossiers = dossiers @dossiers = dossiers
@user_profile = user_profile @user_profile = user_profile
@export_template = export_template
end end
def to_csv def to_csv
@ -36,7 +37,7 @@ class ProcedureExportService
end end
def to_zip 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| DownloadableFileService.download_and_zip(procedure, attachments, base_filename) do |zip_filepath|
ArchiveUploader.new(procedure: procedure, filename: filename(:zip), filepath: zip_filepath).blob ArchiveUploader.new(procedure: procedure, filename: filename(:zip), filepath: zip_filepath).blob

View file

@ -121,7 +121,7 @@ describe Experts::AvisController, type: :controller do
context 'with a valid avis' do context 'with a valid avis' do
it do it do
service = instance_double(PiecesJustificativesService) 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(:generate_dossiers_export).with(Dossier.where(id: dossier)).and_return([])
expect(service).to receive(:liste_documents).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) is_expected.to have_http_status(:success)

View file

@ -936,7 +936,7 @@ describe Instructeurs::DossiersController, type: :controller do
subject subject
end 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(assigns(:is_dossier_in_batch_operation)).to eq(false) }
it { expect(response).to render_template 'dossiers/show' } it { expect(response).to render_template 'dossiers/show' }

View file

@ -736,6 +736,18 @@ describe Instructeurs::ProceduresController, type: :controller do
end end
it { expect { subject }.to change { Export.where(user_profile: instructeur).count }.by(1) } 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 end
context 'when the export is not ready' do context 'when the export is not ready' do

View file

@ -1142,7 +1142,7 @@ describe Users::DossiersController, type: :controller do
end end
context 'when the dossier has been submitted' do 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') } it { expect(response).to render_template('dossiers/show') }
end end
end end

View file

@ -109,6 +109,14 @@ RSpec.describe Export, type: :model do
end end
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 context 'with existing matching export' do
def find_or_create = 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) 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)

View file

@ -2,8 +2,9 @@ describe PiecesJustificativesService do
describe '.liste_documents' do describe '.liste_documents' do
let(:dossier) { create(:dossier, procedure: procedure) } let(:dossier) { create(:dossier, procedure: procedure) }
let(:dossiers) { Dossier.where(id: dossier.id) } let(:dossiers) { Dossier.where(id: dossier.id) }
let(:export_template) { nil }
subject do subject do
PiecesJustificativesService.new(user_profile:).liste_documents(dossiers).map(&:first) PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:first)
end end
context 'no acl' do context 'no acl' do
@ -19,6 +20,11 @@ describe PiecesJustificativesService do
end end
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } 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 end
context 'with a multiple attachments' do 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(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :piece_justificative }] }]) }
let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) } let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) }
let(:dossiers) { Dossier.where(id: dossier.id) } 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 it "doesn't update dossier" do
expect { subject }.not_to change { dossier.updated_at } 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!(:not_confidentiel_avis) { create(:avis, :not_confidentiel, dossier: dossier) }
let!(:expert_avis) { create(:avis, :confidentiel, dossier: dossier, expert: user_profile) } 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 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([]) expect_any_instance_of(Dossier).to receive(:avis_for_expert).with(user_profile).and_return([])
subject subject

View file

@ -2,8 +2,9 @@ require 'csv'
describe ProcedureExportService do describe ProcedureExportService do
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs, instructeurs: [instructeur]) } let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) }
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur) } let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
let(:export_template) { nil }
describe 'to_xlsx' do describe 'to_xlsx' do
subject do subject do
@ -243,7 +244,7 @@ describe ProcedureExportService do
context 'as csv' do context 'as csv' do
subject do subject do
ProcedureExportService.new(procedure, procedure.dossiers, instructeur) ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template)
.to_csv .to_csv
.open { |f| CSV.read(f.path) } .open { |f| CSV.read(f.path) }
end end
@ -519,39 +520,68 @@ describe ProcedureExportService do
end end
end end
context 'generate_dossiers_export' do describe 'generate_dossiers_export' do
it 'include_infos_administration (so it includes avis, champs privés)' 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 subject
end end
end
context 'with files (and http calls)' do context 'with export_template' do
let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } 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)) } let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) }
before do let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) }
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") 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 end
it 'returns a blob with valid files' do context 'with files (and http calls)' do
VCR.use_cassette('archive/new_file_to_get_200') do let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) }
subject 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') it 'returns a blob with valid files' do
File.open('tmp.zip') do |fd| VCR.use_cassette('archive/new_file_to_get_200') do
files = ZipTricks::FileReader.read_zip_structure(io: fd) subject
base_fn = 'export'
structure = [ File.write('tmp.zip', subject.download, mode: 'wb')
"#{base_fn}/", File.open('tmp.zip') do |fd|
"#{base_fn}/dossier-#{dossier.id}/", files = ZipTricks::FileReader.read_zip_structure(io: fd)
"#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", base_fn = 'export'
"#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", structure = [
"#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" "#{base_fn}/",
] "#{base_fn}/dossier-#{dossier.id}/",
expect(files.size).to eq(structure.size) "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/",
expect(files.map(&:filename)).to match_array(structure) "#{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 end
FileUtils.remove_entry_secure('tmp.zip')
end end
end end
end end