Merge pull request #7154 from betagouv/main

2022-04-12-01
This commit is contained in:
Paul Chavard 2022-04-12 19:38:07 +02:00 committed by GitHub
commit 6a1f95b1f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 240 additions and 210 deletions

View file

@ -59,6 +59,7 @@ 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'

View file

@ -459,6 +459,9 @@ 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)
@ -837,6 +840,7 @@ DEPENDENCIES
pg pg
phonelib phonelib
prawn-rails prawn-rails
prawn-svg
premailer-rails premailer-rails
pry-byebug pry-byebug
puma puma

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,10 +1,10 @@
module Types module Types
class AddressType < Types::BaseObject class AddressType < Types::BaseObject
class AddressTypeType < Types::BaseEnum class AddressTypeType < Types::BaseEnum
value(:housenumber, "numéro « à la plaque »", value: :housenumber) value(:housenumber, "numéro « à la plaque »", value: "housenumber")
value(:street, "position « à la voie », placé approximativement au centre de celle-ci", value: :street) value(:street, "position « à la voie », placé approximativement au centre de celle-ci", value: "street")
value(:municipality, "numéro « à la commune »", value: :municipality) value(:municipality, "numéro « à la commune »", value: "municipality")
value(:locality, "lieu-dit", value: :locality) value(:locality, "lieu-dit", value: "locality")
end end
field :label, String, "libellé complet de ladresse", null: false field :label, String, "libellé complet de ladresse", null: false

View file

@ -104,7 +104,7 @@ module Types
def address def address
{ {
label: object.adresse, label: object.adresse,
type: :housenumber, type: "housenumber",
street_number: object.numero_voie, street_number: object.numero_voie,
street_name: object.nom_voie, street_name: object.nom_voie,
street_address: object.nom_voie.present? ? [object.numero_voie, object.type_voie, object.nom_voie].compact.join(' ') : nil, street_address: object.nom_voie.present? ? [object.numero_voie, object.type_voie, object.nom_voie].compact.join(' ') : nil,

View file

@ -1,7 +1,8 @@
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)
PiecesJustificativesService.generate_dossier_export(dossiers) + dossiers
PiecesJustificativesService.liste_documents(dossiers, for_expert) .map { |d| pj_and_path(d.id, PiecesJustificativesService.generate_dossier_export(d)) } +
PiecesJustificativesService.liste_documents(dossiers, for_expert)
end end
private private

View file

@ -485,9 +485,8 @@ class Dossier < ApplicationRecord
end end
def motivation def motivation
if termine? return nil if !termine?
traitement&.motivation || read_attribute(:motivation) traitement&.motivation || read_attribute(:motivation)
end
end end
def update_search_terms def update_search_terms

View file

@ -18,8 +18,9 @@ class Export < ApplicationRecord
enum format: { enum format: {
csv: 'csv', csv: 'csv',
ods: 'ods', ods: 'ods',
xlsx: 'xlsx' xlsx: 'xlsx',
} zip: 'zip'
}, _prefix: true
enum time_span_type: { enum time_span_type: {
everything: 'everything', everything: 'everything',
@ -49,11 +50,11 @@ class Export < ApplicationRecord
FORMATS_WITH_TIME_SPAN = [:xlsx, :ods, :csv].flat_map do |format| FORMATS_WITH_TIME_SPAN = [:xlsx, :ods, :csv].flat_map do |format|
time_span_types.keys.map do |time_span_type| 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
end end
FORMATS = [:xlsx, :ods, :csv].map do |format| FORMATS = [:xlsx, :ods, :csv, :zip].map do |format|
{ format: format.to_sym } { format: format }
end end
def compute_async def compute_async
@ -63,13 +64,7 @@ class Export < ApplicationRecord
def compute def compute
load_snapshot! load_snapshot!
file.attach( file.attach(blob)
io: io,
filename: filename,
content_type: content_type,
# We generate the exports ourselves, so they are safe
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
)
end end
def since def since
@ -92,18 +87,6 @@ class Export < ApplicationRecord
procedure_presentation_id.present? procedure_presentation_id.present?
end 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 self.find_or_create_export(format, groupe_instructeurs, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) 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) create_with(groupe_instructeurs: groupe_instructeurs, procedure_presentation: procedure_presentation, procedure_presentation_snapshot: procedure_presentation&.snapshot)
.includes(:procedure_presentation) .includes(:procedure_presentation)
@ -124,16 +107,20 @@ class Export < ApplicationRecord
{ {
xlsx: { xlsx: {
time_span_type: not_filtered.filter(&:xlsx?).index_by(&:time_span_type), time_span_type: not_filtered.filter(&:format_xlsx?).index_by(&:time_span_type),
statut: filtered.filter(&:xlsx?).index_by(&:statut) statut: filtered.filter(&:format_xlsx?).index_by(&:statut)
}, },
ods: { ods: {
time_span_type: not_filtered.filter(&:ods?).index_by(&:time_span_type), time_span_type: not_filtered.filter(&:format_ods?).index_by(&:time_span_type),
statut: filtered.filter(&:ods?).index_by(&:statut) statut: filtered.filter(&:format_ods?).index_by(&:statut)
}, },
csv: { csv: {
time_span_type: not_filtered.filter(&:csv?).index_by(&:time_span_type), time_span_type: not_filtered.filter(&:format_csv?).index_by(&:time_span_type),
statut: filtered.filter(&:csv?).index_by(&:statut) statut: filtered.filter(&:format_csv?).index_by(&:statut)
},
zip: {
time_span_type: {},
statut: filtered.filter(&:format_zip?).index_by(&:statut)
} }
} }
end end
@ -177,32 +164,18 @@ class Export < ApplicationRecord
end end
end end
def filename def blob
procedure_identifier = procedure.path || "procedure-#{procedure.id}"
"dossiers_#{procedure_identifier}_#{statut}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}"
end
def io
service = ProcedureExportService.new(procedure, dossiers_for_export) service = ProcedureExportService.new(procedure, dossiers_for_export)
case format.to_sym case format.to_sym
when :csv when :csv
StringIO.new(service.to_csv) service.to_csv
when :xlsx when :xlsx
StringIO.new(service.to_xlsx) service.to_xlsx
when :ods when :ods
StringIO.new(service.to_ods) service.to_ods
end when :zip
end service.to_zip
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'
end end
end end

View file

@ -4,7 +4,7 @@ class ArchiveUploader
# when file size is bigger, active storage expects the chunks + a manifest. # 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 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 uploaded_blob = create_and_upload_blob
begin begin
archive.file.purge if archive.file.attached? archive.file.purge if archive.file.attached?
@ -21,9 +21,13 @@ class ArchiveUploader
) )
end end
def blob
create_and_upload_blob
end
private private
attr_reader :procedure, :archive, :filepath attr_reader :procedure, :filename, :filepath
def create_and_upload_blob def create_and_upload_blob
if active_storage_service_local? || File.size(filepath) < MAX_FILE_SIZE_FOR_BACKEND_BEFORE_CHUNKING 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) def blob_default_params(filepath)
{ {
key: namespaced_object_key, key: namespaced_object_key,
filename: archive.filename(procedure), filename: filename,
content_type: 'application/zip', content_type: 'application/zip',
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } 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) system(ENV.fetch('ACTIVE_STORAGE_BIG_FILE_UPLOADER_WITH_ENCRYPTION_PATH').to_s, filepath, blob.key, exception: true)
end end
def initialize(procedure:, archive:, filepath:) def initialize(procedure:, filename:, filepath:)
@procedure = procedure @procedure = procedure
@archive = archive @filename = filename
@filepath = filepath @filepath = filepath
end end
end end

View file

@ -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

View file

@ -107,44 +107,21 @@ class PiecesJustificativesService
end end
end end
def self.generate_dossier_export(dossiers) def self.generate_dossier_export(dossier)
return [] if dossiers.empty? pdf = ApplicationController
.render(template: 'dossiers/show', formats: [:pdf],
assigns: {
include_infos_administration: true,
dossier: dossier
})
pdfs = [] FakeAttachment.new(
file: StringIO.new(pdf),
procedure = dossiers.first.procedure filename: "export-#{dossier.id}.pdf",
tdc_by_id = TypeDeChamp name: 'pdf_export_for_instructeur',
.joins(:revisions) id: dossier.id,
.where(revisions: { id: procedure.revisions }) created_at: dossier.updated_at
.to_a )
.group_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

View file

@ -1,8 +1,6 @@
require 'tempfile' require 'tempfile'
class ProcedureArchiveService class ProcedureArchiveService
ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' }
def initialize(procedure) def initialize(procedure)
@procedure = procedure @procedure = procedure
end end
@ -27,9 +25,9 @@ class ProcedureArchiveService
attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers) attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers)
download_and_zip(archive, attachments) do |zip_filepath| DownloadableFileService.download_and_zip(@procedure, attachments, zip_root_folder(archive)) do |zip_filepath|
ArchiveUploader.new(procedure: @procedure, archive: archive, filepath: zip_filepath) ArchiveUploader.new(procedure: @procedure, filename: archive.filename(@procedure), filepath: zip_filepath)
.upload .upload(archive)
end end
end end
@ -45,30 +43,6 @@ class ProcedureArchiveService
private 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) def zip_root_folder(archive)
"procedure-#{@procedure.id}-#{archive.id}" "procedure-#{@procedure.id}-#{archive.id}"
end end

View file

@ -1,5 +1,5 @@
class ProcedureExportService class ProcedureExportService
attr_reader :dossiers attr_reader :procedure, :dossiers
def initialize(procedure, dossiers) def initialize(procedure, dossiers)
@procedure = procedure @procedure = procedure
@ -8,25 +8,72 @@ class ProcedureExportService
end end
def to_csv 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 end
def to_xlsx def to_xlsx
# We recursively build multi page spreadsheet # 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) SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package)
end.to_stream.read end.to_stream
create_blob(io, :xlsx)
end end
def to_ods def to_ods
# We recursively build multi page spreadsheet # 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) SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet)
end.bytes 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 end
private 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'
when :zip
'application/zip'
end
end
def etablissements def etablissements
@etablissements ||= dossiers.flat_map do |dossier| @etablissements ||= dossiers.flat_map do |dossier|
[dossier.champs, dossier.champs_private] [dossier.champs, dossier.champs_private]
@ -40,12 +87,12 @@ class ProcedureExportService
end end
def champs_repetables_options def champs_repetables_options
revision = @procedure.active_revision revision = procedure.active_revision
champs_by_stable_id = dossiers champs_by_stable_id = dossiers
.flat_map { |dossier| (dossier.champs + dossier.champs_private).filter(&:repetition?) } .flat_map { |dossier| (dossier.champs + dossier.champs_private).filter(&:repetition?) }
.group_by(&:stable_id) .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] } .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? } .filter { |(_, types_de_champ)| types_de_champ.present? }
.map do |(type_de_champ_repetition, types_de_champ)| .map do |(type_de_champ_repetition, types_de_champ)|
@ -85,7 +132,7 @@ class ProcedureExportService
end end
def spreadsheet_columns(format) 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| Proc.new do |instance|
instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ) instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ)

View file

@ -181,7 +181,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.image DOSSIER_PDF_EXPORT_LOGO_SRC, width: 300, position: :center pdf.svg IO.read(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)

View file

@ -130,30 +130,28 @@ 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].first
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, tdc.libelle) add_section_title(pdf, champ.libelle)
when 'Champs::ExplicationChamp' when 'Champs::ExplicationChamp'
format_in_2_lines(pdf, tdc.libelle, tdc.description) format_in_2_lines(pdf, champ.libelle, champ.description)
when 'Champs::CarteChamp' when 'Champs::CarteChamp'
format_in_2_lines(pdf, tdc.libelle, champ.to_feature_collection.to_json) format_in_2_lines(pdf, champ.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 tdc.libelle pdf.text champ.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, tdc.libelle, value) format_in_2_lines(pdf, champ.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, tdc.libelle, value) format_in_2_lines(pdf, champ.libelle, value)
end end
end end
@ -207,9 +205,6 @@ 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.champs.map(&:type_de_champ).group_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' ),
@ -217,12 +212,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.image DOSSIER_PDF_EXPORT_LOGO_SRC, width: 300, position: :center pdf.svg IO.read(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', @procedure.libelle) format_in_2_columns(pdf, 'Démarche', @dossier.procedure.libelle)
format_in_2_columns(pdf, 'Organisme', @procedure.organisation_name) format_in_2_columns(pdf, 'Organisme', @dossier.procedure.organisation_name)
add_etat_dossier(pdf, @dossier) add_etat_dossier(pdf, @dossier)

View file

@ -69,7 +69,7 @@ DS_ENV="staging"
# PROCEDURE_DEFAULT_LOGO_SRC="republique-francaise-logo.svg" # PROCEDURE_DEFAULT_LOGO_SRC="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.png" # DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg"
# Instance customization: watermark for identity documents # Instance customization: watermark for identity documents
# WATERMARK_FILE="" # WATERMARK_FILE=""

View file

@ -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") PROCEDURE_DEFAULT_LOGO_SRC = ENV.fetch("PROCEDURE_DEFAULT_LOGO_SRC", "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.png") DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.svg")

View file

@ -5,6 +5,7 @@ fr:
everything_csv_html: Demander un export au format .csv<br>(uniquement les dossiers, sans les champs répétables) everything_csv_html: Demander un export au format .csv<br>(uniquement les dossiers, sans les champs répétables)
everything_xlsx_html: Demander un export au format .xlsx everything_xlsx_html: Demander un export au format .xlsx
everything_ods_html: Demander un export au format .ods 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_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é<br>(demandé il y a %{export_time}) everything_pending_html: Un export au format %{export_format} est en train dêtre généré<br>(demandé il y a %{export_time})
everything_ready_html: Télécharger lexport au format %{export_format}<br>(généré il y a %{export_time}) everything_ready_html: Télécharger lexport au format %{export_format}<br>(généré il y a %{export_time})

View file

@ -31,6 +31,26 @@ RSpec.describe Types::DossierType, type: :graphql do
let(:dossier) { create(:dossier, :accepte, :with_populated_champs, procedure: procedure) } let(:dossier) { create(:dossier, :accepte, :with_populated_champs, procedure: procedure) }
let(:query) { DOSSIER_WITH_CHAMPS_QUERY } let(:query) { DOSSIER_WITH_CHAMPS_QUERY }
let(:variables) { { number: dossier.id } } 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][0][:__typename]).to eq "CommuneChamp" }
it { expect(data[:dossier][:champs][1][:__typename]).to eq "AddressChamp" } it { expect(data[:dossier][:champs][1][:__typename]).to eq "AddressChamp" }
@ -86,7 +106,12 @@ RSpec.describe Types::DossierType, type: :graphql do
code code
} }
fragment AddressFragment on Address { fragment AddressFragment on Address {
type
label
cityName cityName
cityCode
streetName
streetNumber
} }
GRAPHQL GRAPHQL
end end

View file

@ -11,7 +11,7 @@ describe ArchiveCreationJob, type: :job do
before { expect(InstructeurMailer).not_to receive(:send_archive) } before { expect(InstructeurMailer).not_to receive(:send_archive) }
it 'does not send email and forward error for retry' do 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 { job.perform_now }.to raise_error(StandardError, "kaboom")
expect(archive.reload.failed?).to eq(true) expect(archive.reload.failed?).to eq(true)
end end
@ -20,7 +20,7 @@ describe ArchiveCreationJob, type: :job do
context 'when it works' do context 'when it works' do
let(:mailer) { double('mailer', deliver_later: true) } let(:mailer) { double('mailer', deliver_later: true) }
before do 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) expect(InstructeurMailer).to receive(:send_archive).and_return(mailer)
end end

View file

@ -48,9 +48,9 @@ RSpec.describe Export, type: :model do
context 'when an export is made for one groupe instructeur' do context 'when an export is made for one groupe instructeur' do
let!(:export) { create(:export, groupe_instructeurs: [gi_1, gi_2]) } 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_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: {} } }) } 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: {} } }) } 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 end
end end

View file

@ -4,18 +4,18 @@ describe ProcedureArchiveService do
let(:file) { Tempfile.new } 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(: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 describe '.upload' do
context 'when active storage service is local' do context 'when active storage service is local' do
it 'uploads with upload_with_active_storage' do it 'uploads with upload_with_active_storage' do
expect(uploader).to receive(:active_storage_service_local?).and_return(true) expect(uploader).to receive(:active_storage_service_local?).and_return(true)
expect(uploader).to receive(:upload_with_active_storage).and_return(fixture_blob) expect(uploader).to receive(:upload_with_active_storage).and_return(fixture_blob)
uploader.upload uploader.upload(archive)
end end
it 'link the created blob as an attachment to the current archive instance' do 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) .to change { ActiveStorage::Attachment.where(name: 'file', record_type: 'Archive', record_id: archive.id).count }.by(1)
end end
end end
@ -31,7 +31,7 @@ describe ProcedureArchiveService do
it 'uploads with upload_with_active_storage' do it 'uploads with upload_with_active_storage' do
expect(uploader).to receive(:upload_with_active_storage).and_return(fixture_blob) expect(uploader).to receive(:upload_with_active_storage).and_return(fixture_blob)
uploader.upload uploader.upload(archive)
end end
end end
@ -40,12 +40,12 @@ describe ProcedureArchiveService do
it 'uploads with upload_with_chunking_wrapper' do it 'uploads with upload_with_chunking_wrapper' do
expect(uploader).to receive(:upload_with_chunking_wrapper).and_return(fixture_blob) expect(uploader).to receive(:upload_with_chunking_wrapper).and_return(fixture_blob)
uploader.upload uploader.upload(archive)
end end
it 'link the created blob as an attachment to the current archive instance' do 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).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) .to change { ActiveStorage::Attachment.where(name: 'file', record_type: 'Archive', record_id: archive.id).count }.by(1)
end end
end end

View file

@ -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

View file

@ -192,7 +192,7 @@ describe PiecesJustificativesService do
describe '.generate_dossier_export' do describe '.generate_dossier_export' do
let(:dossier) { create(:dossier) } 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 it "doesn't update dossier" do
expect { subject }.not_to change { dossier.updated_at } expect { subject }.not_to change { dossier.updated_at }

View file

@ -172,37 +172,6 @@ describe ProcedureArchiveService do
end end
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 private
def create_dossier_for_month(year, month) def create_dossier_for_month(year, month)

View file

@ -4,11 +4,9 @@ describe ProcedureExportService do
describe 'to_data' do describe 'to_data' do
let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) }
subject do subject do
Tempfile.create do |f| ProcedureExportService.new(procedure, procedure.dossiers)
f << ProcedureExportService.new(procedure, procedure.dossiers).to_xlsx .to_xlsx
f.rewind .open { |f| SimpleXlsxReader.open(f.path) }
SimpleXlsxReader.open(f.path)
end
end end
let(:dossiers_sheet) { subject.sheets.first } let(:dossiers_sheet) { subject.sheets.first }
@ -178,11 +176,9 @@ describe ProcedureExportService do
context 'as csv' do context 'as csv' do
subject do subject do
Tempfile.create do |f| ProcedureExportService.new(procedure, procedure.dossiers)
f << ProcedureExportService.new(procedure, procedure.dossiers).to_csv .to_csv
f.rewind .open { |f| CSV.read(f.path) }
CSV.read(f.path)
end
end end
let(:nominal_headers) do let(:nominal_headers) do