Merge pull request #7586 from tchak/perf-faster-pdf-export
Faster pdf export
This commit is contained in:
commit
93e4e98fea
16 changed files with 88 additions and 55 deletions
1
Gemfile
1
Gemfile
|
@ -62,7 +62,6 @@ gem 'openid_connect'
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
gem 'phonelib'
|
gem 'phonelib'
|
||||||
gem 'prawn-rails' # PDF Generation
|
gem 'prawn-rails' # PDF Generation
|
||||||
gem 'prawn-svg'
|
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'puma' # Use Puma as the app server
|
gem 'puma' # Use Puma as the app server
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
|
|
|
@ -478,9 +478,6 @@ GEM
|
||||||
prawn
|
prawn
|
||||||
prawn-table
|
prawn-table
|
||||||
rails (>= 3.1.0)
|
rails (>= 3.1.0)
|
||||||
prawn-svg (0.31.0)
|
|
||||||
css_parser (~> 1.6)
|
|
||||||
prawn (>= 0.11.1, < 3)
|
|
||||||
prawn-table (0.2.2)
|
prawn-table (0.2.2)
|
||||||
prawn (>= 1.3.0, < 3.0.0)
|
prawn (>= 1.3.0, < 3.0.0)
|
||||||
premailer (1.14.2)
|
premailer (1.14.2)
|
||||||
|
@ -868,7 +865,6 @@ DEPENDENCIES
|
||||||
pg
|
pg
|
||||||
phonelib
|
phonelib
|
||||||
prawn-rails
|
prawn-rails
|
||||||
prawn-svg
|
|
||||||
premailer-rails
|
premailer-rails
|
||||||
pry-byebug
|
pry-byebug
|
||||||
puma
|
puma
|
||||||
|
|
BIN
app/assets/images/header/logo-ds-wide.png
Normal file
BIN
app/assets/images/header/logo-ds-wide.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -1,8 +1,7 @@
|
||||||
class ActiveStorage::DownloadableFile
|
class ActiveStorage::DownloadableFile
|
||||||
def self.create_list_from_dossiers(dossiers, for_expert = false)
|
def self.create_list_from_dossiers(dossiers, for_expert = false)
|
||||||
dossiers
|
PiecesJustificativesService.generate_dossier_export(dossiers) +
|
||||||
.map { |d| pj_and_path(d.id, PiecesJustificativesService.generate_dossier_export(d)) } +
|
PiecesJustificativesService.liste_documents(dossiers, for_expert)
|
||||||
PiecesJustificativesService.liste_documents(dossiers, for_expert)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -553,8 +553,9 @@ class Dossier < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def motivation
|
def motivation
|
||||||
return nil if !termine?
|
if termine?
|
||||||
traitement&.motivation || read_attribute(:motivation)
|
traitement&.motivation || read_attribute(:motivation)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_search_terms
|
def update_search_terms
|
||||||
|
|
|
@ -107,21 +107,44 @@ class PiecesJustificativesService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.generate_dossier_export(dossier)
|
def self.generate_dossier_export(dossiers)
|
||||||
pdf = ApplicationController
|
return [] if dossiers.empty?
|
||||||
.render(template: 'dossiers/show', formats: [:pdf],
|
|
||||||
assigns: {
|
|
||||||
include_infos_administration: true,
|
|
||||||
dossier: dossier
|
|
||||||
})
|
|
||||||
|
|
||||||
FakeAttachment.new(
|
pdfs = []
|
||||||
file: StringIO.new(pdf),
|
|
||||||
filename: "export-#{dossier.id}.pdf",
|
procedure = dossiers.first.procedure
|
||||||
name: 'pdf_export_for_instructeur',
|
tdc_by_id = TypeDeChamp
|
||||||
id: dossier.id,
|
.joins(:revisions)
|
||||||
created_at: dossier.updated_at
|
.where(revisions: { id: procedure.revisions })
|
||||||
)
|
.to_a
|
||||||
|
.index_by(&:id)
|
||||||
|
|
||||||
|
dossiers
|
||||||
|
.includes(:champs, :champs_private, :commentaires, :individual,
|
||||||
|
:traitement, :etablissement,
|
||||||
|
user: :france_connect_information, avis: :expert)
|
||||||
|
.find_each do |dossier|
|
||||||
|
pdf = ApplicationController
|
||||||
|
.render(template: 'dossiers/show', formats: [:pdf],
|
||||||
|
assigns: {
|
||||||
|
include_infos_administration: true,
|
||||||
|
dossier: dossier,
|
||||||
|
procedure: procedure,
|
||||||
|
tdc_by_id: tdc_by_id
|
||||||
|
})
|
||||||
|
|
||||||
|
a = FakeAttachment.new(
|
||||||
|
file: StringIO.new(pdf),
|
||||||
|
filename: "export-#{dossier.id}.pdf",
|
||||||
|
name: 'pdf_export_for_instructeur',
|
||||||
|
id: dossier.id,
|
||||||
|
created_at: dossier.updated_at
|
||||||
|
)
|
||||||
|
|
||||||
|
pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a)
|
||||||
|
end
|
||||||
|
|
||||||
|
pdfs
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -206,7 +206,7 @@ prawn_document(page_size: "A4") do |pdf|
|
||||||
italic: Rails.root.join('lib/prawn/fonts/marianne/marianne-thin.ttf' ),
|
italic: Rails.root.join('lib/prawn/fonts/marianne/marianne-thin.ttf' ),
|
||||||
})
|
})
|
||||||
pdf.font 'marianne'
|
pdf.font 'marianne'
|
||||||
pdf.svg IO.read(DOSSIER_PDF_EXPORT_LOGO_SRC), width: 300, position: :center
|
pdf.image DOSSIER_PDF_EXPORT_LOGO_SRC, width: 300, position: :center
|
||||||
pdf.move_down(40)
|
pdf.move_down(40)
|
||||||
|
|
||||||
render_in_2_columns(pdf, 'Démarche', @dossier.procedure.libelle)
|
render_in_2_columns(pdf, 'Démarche', @dossier.procedure.libelle)
|
||||||
|
|
|
@ -130,28 +130,30 @@ def add_identite_etablissement(pdf, etablissement)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_single_champ(pdf, champ)
|
def add_single_champ(pdf, champ)
|
||||||
|
tdc = @tdc_by_id[champ.type_de_champ_id]
|
||||||
|
|
||||||
case champ.type
|
case champ.type
|
||||||
when 'Champs::PieceJustificativeChamp', 'Champs::TitreIdentiteChamp'
|
when 'Champs::PieceJustificativeChamp', 'Champs::TitreIdentiteChamp'
|
||||||
return
|
return
|
||||||
when 'Champs::HeaderSectionChamp'
|
when 'Champs::HeaderSectionChamp'
|
||||||
add_section_title(pdf, champ.libelle)
|
add_section_title(pdf, tdc.libelle)
|
||||||
when 'Champs::ExplicationChamp'
|
when 'Champs::ExplicationChamp'
|
||||||
format_in_2_lines(pdf, champ.libelle, champ.description)
|
format_in_2_lines(pdf, tdc.libelle, tdc.description)
|
||||||
when 'Champs::CarteChamp'
|
when 'Champs::CarteChamp'
|
||||||
format_in_2_lines(pdf, champ.libelle, champ.to_feature_collection.to_json)
|
format_in_2_lines(pdf, tdc.libelle, champ.to_feature_collection.to_json)
|
||||||
when 'Champs::SiretChamp'
|
when 'Champs::SiretChamp'
|
||||||
pdf.font 'marianne', style: :bold do
|
pdf.font 'marianne', style: :bold do
|
||||||
pdf.text champ.libelle
|
pdf.text tdc.libelle
|
||||||
end
|
end
|
||||||
if champ.etablissement.present?
|
if champ.etablissement.present?
|
||||||
add_identite_etablissement(pdf, champ.etablissement)
|
add_identite_etablissement(pdf, champ.etablissement)
|
||||||
end
|
end
|
||||||
when 'Champs::NumberChamp'
|
when 'Champs::NumberChamp'
|
||||||
value = champ.to_s.empty? ? 'Non communiqué' : number_with_delimiter(champ.to_s)
|
value = champ.to_s.empty? ? 'Non communiqué' : number_with_delimiter(champ.to_s)
|
||||||
format_in_2_lines(pdf, champ.libelle, value)
|
format_in_2_lines(pdf, tdc.libelle, value)
|
||||||
else
|
else
|
||||||
value = champ.to_s.empty? ? 'Non communiqué' : champ.to_s
|
value = champ.to_s.empty? ? 'Non communiqué' : champ.to_s
|
||||||
format_in_2_lines(pdf, champ.libelle, value)
|
format_in_2_lines(pdf, tdc.libelle, value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -205,6 +207,9 @@ def add_etats_dossier(pdf, dossier)
|
||||||
end
|
end
|
||||||
|
|
||||||
prawn_document(page_size: "A4") do |pdf|
|
prawn_document(page_size: "A4") do |pdf|
|
||||||
|
@procedure ||= @dossier.procedure
|
||||||
|
@tdc_by_id ||= @dossier.revision.types_de_champ.index_by(&:id)
|
||||||
|
|
||||||
pdf.font_families.update( 'marianne' => {
|
pdf.font_families.update( 'marianne' => {
|
||||||
normal: Rails.root.join('lib/prawn/fonts/marianne/marianne-regular.ttf' ),
|
normal: Rails.root.join('lib/prawn/fonts/marianne/marianne-regular.ttf' ),
|
||||||
bold: Rails.root.join('lib/prawn/fonts/marianne/marianne-bold.ttf' ),
|
bold: Rails.root.join('lib/prawn/fonts/marianne/marianne-bold.ttf' ),
|
||||||
|
@ -212,12 +217,12 @@ prawn_document(page_size: "A4") do |pdf|
|
||||||
pdf.font 'marianne'
|
pdf.font 'marianne'
|
||||||
|
|
||||||
pdf.pad_bottom(40) do
|
pdf.pad_bottom(40) do
|
||||||
pdf.svg IO.read(DOSSIER_PDF_EXPORT_LOGO_SRC), width: 300, position: :center
|
pdf.image DOSSIER_PDF_EXPORT_LOGO_SRC, width: 300, position: :center
|
||||||
end
|
end
|
||||||
|
|
||||||
format_in_2_columns(pdf, 'Dossier Nº', @dossier.id.to_s)
|
format_in_2_columns(pdf, 'Dossier Nº', @dossier.id.to_s)
|
||||||
format_in_2_columns(pdf, 'Démarche', @dossier.procedure.libelle)
|
format_in_2_columns(pdf, 'Démarche', @procedure.libelle)
|
||||||
format_in_2_columns(pdf, 'Organisme', @dossier.procedure.organisation_name)
|
format_in_2_columns(pdf, 'Organisme', @procedure.organisation_name)
|
||||||
|
|
||||||
add_etat_dossier(pdf, @dossier)
|
add_etat_dossier(pdf, @dossier)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ prawn_document(margin: [top_margin, right_margin, bottom_margin, left_margin], p
|
||||||
black = '333333'
|
black = '333333'
|
||||||
|
|
||||||
pdf.float do
|
pdf.float do
|
||||||
pdf.svg IO.read(DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC), height: 64
|
pdf.image DOSSIER_PDF_EXPORT_LOGO_SRC, height: 64
|
||||||
end
|
end
|
||||||
|
|
||||||
pdf.bounding_box([110, pdf.cursor - 18], width: header_width - 200) do
|
pdf.bounding_box([110, pdf.cursor - 18], width: header_width - 200) do
|
||||||
|
|
|
@ -72,7 +72,7 @@ DS_ENV="staging"
|
||||||
# DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC="app/assets/images/republique-francaise-logo.svg"
|
# DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC="app/assets/images/republique-francaise-logo.svg"
|
||||||
|
|
||||||
# Instance customization: PDF export logo ---> to be put in "app/assets/images"
|
# Instance customization: PDF export logo ---> to be put in "app/assets/images"
|
||||||
# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg"
|
# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.png"
|
||||||
|
|
||||||
# Instance customization: watermark for identity documents
|
# Instance customization: watermark for identity documents
|
||||||
# WATERMARK_FILE=""
|
# WATERMARK_FILE=""
|
||||||
|
|
|
@ -20,4 +20,4 @@ PROCEDURE_DEFAULT_LOGO_SRC = ENV.fetch("PROCEDURE_DEFAULT_LOGO_SRC", "republique
|
||||||
DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC = ENV.fetch("DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC", "app/assets/images/republique-francaise-logo.svg")
|
DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC = ENV.fetch("DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC", "app/assets/images/republique-francaise-logo.svg")
|
||||||
|
|
||||||
# Logo in PDF export of a "Dossier"
|
# 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.svg")
|
DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.png")
|
||||||
|
|
|
@ -263,9 +263,9 @@ describe API::V1::DossiersController do
|
||||||
it 'should have rows' do
|
it 'should have rows' do
|
||||||
expect(subject.size).to eq(2)
|
expect(subject.size).to eq(2)
|
||||||
expect(subject[0][:id]).to eq(1)
|
expect(subject[0][:id]).to eq(1)
|
||||||
expect(subject[0][:champs].size).to eq(2)
|
expect(subject[0][:champs].size).to eq(3)
|
||||||
expect(subject[0][:champs].map { |c| c[:value] }).to eq(['text', '42'])
|
expect(subject[0][:champs].map { |c| c[:value] }).to eq(['text', 'text', '42'])
|
||||||
expect(subject[0][:champs].map { |c| c[:type_de_champ][:type_champ] }).to eq(['text', 'number'])
|
expect(subject[0][:champs].map { |c| c[:type_de_champ][:type_champ] }).to eq(['text', 'text', 'number'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -226,6 +226,7 @@ FactoryBot.define do
|
||||||
position: 0,
|
position: 0,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
libelle: 'Nom')
|
libelle: 'Nom')
|
||||||
|
types_de_champ.push(type_de_champ_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
type_de_champ_number = types_de_champ.find { |tdc| tdc.libelle == 'Age' }
|
type_de_champ_number = types_de_champ.find { |tdc| tdc.libelle == 'Age' }
|
||||||
|
@ -235,13 +236,13 @@ FactoryBot.define do
|
||||||
position: 1,
|
position: 1,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
libelle: 'Age')
|
libelle: 'Age')
|
||||||
|
types_de_champ.push(type_de_champ_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
evaluator.rows.times do |row|
|
evaluator.rows.times do |row|
|
||||||
champ_repetition.champs << [
|
champ_repetition.champs << types_de_champ.map do |type_de_champ|
|
||||||
build(:champ_text, dossier: champ_repetition.dossier, row: row, type_de_champ: type_de_champ_text, parent: champ_repetition),
|
build(:"champ_#{type_de_champ.type_champ}", dossier: champ_repetition.dossier, row: row, type_de_champ: type_de_champ, parent: champ_repetition)
|
||||||
build(:champ_number, dossier: champ_repetition.dossier, row: row, type_de_champ: type_de_champ_number, parent: champ_repetition)
|
end
|
||||||
]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,19 +10,22 @@ FactoryBot.define do
|
||||||
procedure { nil }
|
procedure { nil }
|
||||||
position { nil }
|
position { nil }
|
||||||
parent { nil }
|
parent { nil }
|
||||||
|
no_coordinate { false }
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:build) do |type_de_champ, evaluator|
|
after(:build) do |type_de_champ, evaluator|
|
||||||
revision = evaluator.procedure&.active_revision || build(:procedure_revision)
|
if !evaluator.no_coordinate
|
||||||
evaluator.procedure&.save
|
revision = evaluator.procedure&.active_revision || build(:procedure_revision)
|
||||||
|
evaluator.procedure&.save
|
||||||
|
|
||||||
revision.revision_types_de_champ << build(:procedure_revision_type_de_champ,
|
revision.revision_types_de_champ << build(:procedure_revision_type_de_champ,
|
||||||
position: evaluator.position || 0,
|
position: evaluator.position || 0,
|
||||||
revision: revision,
|
revision: revision,
|
||||||
type_de_champ: type_de_champ,
|
type_de_champ: type_de_champ,
|
||||||
parent: evaluator.parent)
|
parent: evaluator.parent)
|
||||||
|
|
||||||
revision.save
|
revision.save
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
trait :private do
|
trait :private do
|
||||||
|
|
|
@ -190,9 +190,14 @@ describe PiecesJustificativesService do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.generate_dossier_export' do
|
describe '.generate_dossier_export' do
|
||||||
let(:dossier) { create(:dossier) }
|
let(:procedure) do
|
||||||
|
create(:procedure, :with_piece_justificative).tap do |procedure|
|
||||||
|
create(:type_de_champ_repetition, procedure: procedure, types_de_champ: [create(:type_de_champ_piece_justificative, procedure: procedure, no_coordinate: true)])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) }
|
||||||
|
|
||||||
subject { PiecesJustificativesService.generate_dossier_export(dossier) }
|
subject { PiecesJustificativesService.generate_dossier_export(Dossier.where(id: dossier.id)) }
|
||||||
|
|
||||||
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 }
|
||||||
|
|
|
@ -437,6 +437,7 @@ describe ProcedureExportService do
|
||||||
|
|
||||||
context 'with files (and http calls)' do
|
context 'with files (and http calls)' 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.generate_dossier_export(Dossier.where(id: dossier)) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
|
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
|
||||||
|
@ -454,7 +455,7 @@ describe ProcedureExportService do
|
||||||
"#{service.send(:base_filename)}/dossier-#{dossier.id}/",
|
"#{service.send(:base_filename)}/dossier-#{dossier.id}/",
|
||||||
"#{service.send(:base_filename)}/dossier-#{dossier.id}/pieces_justificatives/",
|
"#{service.send(:base_filename)}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||||
"#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}",
|
"#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}",
|
||||||
"#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(PiecesJustificativesService.generate_dossier_export(dossier))}"
|
"#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}"
|
||||||
]
|
]
|
||||||
expect(files.size).to eq(structure.size)
|
expect(files.size).to eq(structure.size)
|
||||||
expect(files.map(&:filename)).to match_array(structure)
|
expect(files.map(&:filename)).to match_array(structure)
|
||||||
|
|
Loading…
Reference in a new issue