Merge pull request #10217 from demarches-simplifiees/9827-export-rename
ETQ instructeur, je peux renommer le contenu de mon export zip
This commit is contained in:
commit
2761d18de0
64 changed files with 1780 additions and 256 deletions
|
@ -20,7 +20,7 @@
|
|||
min-height: 400px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
.tiptap-editor {
|
||||
// Visual zones
|
||||
.header .flex-1,
|
||||
h1 {
|
||||
|
@ -63,17 +63,6 @@
|
|||
li p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Tags
|
||||
.fr-menu__list {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.fr-tag:not(.fr-menu .fr-tag) {
|
||||
// style span rendered by tiptap like a button/link tag
|
||||
color: var(--text-action-high-blue-france);
|
||||
background-color: var(--background-action-low-blue-france);
|
||||
}
|
||||
}
|
||||
|
||||
// scss-lint:disable SelectorFormat
|
||||
|
|
71
app/assets/stylesheets/exports.scss
Normal file
71
app/assets/stylesheets/exports.scss
Normal file
|
@ -0,0 +1,71 @@
|
|||
@import "constants";
|
||||
|
||||
.export-template-preview {
|
||||
// From https://codepen.io/myramoki/pen/xZJjrr
|
||||
.tree {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tree,
|
||||
.tree ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tree ul {
|
||||
margin: 0 0 0 0.5em; // (indentation/2)
|
||||
}
|
||||
|
||||
.tree:before,
|
||||
.tree ul:before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 4px;
|
||||
border-left: 1px dashed;
|
||||
}
|
||||
|
||||
ul.tree:before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.tree li {
|
||||
margin: 0;
|
||||
padding: 0 1.5em; // indentation + .5em
|
||||
line-height: 2em; // default list item's `line-height`
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tree > li {
|
||||
padding-left: 0; // Don't indent first level
|
||||
}
|
||||
|
||||
.tree li:before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 10px; // same with indentation
|
||||
height: 0;
|
||||
border-top: 1px dashed;
|
||||
margin-top: -1px; // border top width
|
||||
position: absolute;
|
||||
top: 1em; // (line-height/2)
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
ul.tree > li:before {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tree li:last-child:before {
|
||||
background: var(
|
||||
--background-alt-blue-france
|
||||
); // same with body background
|
||||
height: auto;
|
||||
top: 1em; // (line-height/2)
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
14
app/assets/stylesheets/tiptap_editor.scss
Normal file
14
app/assets/stylesheets/tiptap_editor.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
@import "constants";
|
||||
|
||||
.tiptap-editor {
|
||||
// Tags
|
||||
.fr-menu__list {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.fr-tag:not(.fr-menu .fr-tag) {
|
||||
// style span rendered by tiptap like a button/link tag
|
||||
color: var(--text-action-high-blue-france);
|
||||
background-color: var(--background-action-low-blue-france);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
class Dossiers::ExportDropdownComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
|
||||
def initialize(procedure:, statut: nil, count: nil, class_btn: nil, export_url: nil)
|
||||
def initialize(procedure:, export_templates: nil, statut: nil, count: nil, class_btn: nil, export_url: nil)
|
||||
@procedure = procedure
|
||||
@export_templates = export_templates
|
||||
@statut = statut
|
||||
@count = count
|
||||
@class_btn = class_btn
|
||||
|
@ -21,10 +22,15 @@ class Dossiers::ExportDropdownComponent < ApplicationComponent
|
|||
item.fetch(:format) != :json || @procedure.active_revision.carte?
|
||||
end
|
||||
|
||||
def download_export_path(export_format:, no_progress_notification: nil)
|
||||
def download_export_path(export_format: nil, export_template_id: nil, no_progress_notification: nil)
|
||||
@export_url.call(@procedure,
|
||||
export_format: export_format,
|
||||
export_format:,
|
||||
export_template_id:,
|
||||
statut: @statut,
|
||||
no_progress_notification: no_progress_notification)
|
||||
end
|
||||
|
||||
def export_templates
|
||||
@export_templates
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,3 +14,13 @@
|
|||
- menu.with_item do
|
||||
= link_to download_export_path(export_format: format), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
|
||||
= t(".everything_#{format}_html")
|
||||
|
||||
- if export_templates.present?
|
||||
- export_templates.each do |export_template|
|
||||
- menu.with_item do
|
||||
= link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
|
||||
= "Exporter à partir du modèle #{export_template.name}"
|
||||
- if feature_enabled?(:export_template)
|
||||
- menu.with_item do
|
||||
= link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do
|
||||
Ajouter un modèle d'export
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
- each_category do |category, tags, can_toggle_nullable|
|
||||
.flex
|
||||
%p.fr-label.fr-text--sm.fr-text--bold.fr-mb-1w= t(category, scope: ".categories")
|
||||
- if category.present?
|
||||
.flex
|
||||
%p.fr-label.fr-text--sm.fr-text--bold.fr-mb-1w= t(category, scope: ".categories")
|
||||
|
||||
- if can_toggle_nullable
|
||||
.fr-fieldset__element.fr-ml-4w
|
||||
.fr-checkbox-group.fr-checkbox-group--sm
|
||||
= check_box_tag("show_maybe_null", 1, false, data: { "no-autosubmit" => true, action: "change->attestation#toggleMaybeNull"})
|
||||
= label_tag "show_maybe_null", for: :show_maybe_null do
|
||||
Voir les champs facultatifs
|
||||
%span.hidden.fr-hint-text Un champ non rempli restera vide dans l’attestation.
|
||||
- if can_toggle_nullable
|
||||
.fr-fieldset__element.fr-ml-4w
|
||||
.fr-checkbox-group.fr-checkbox-group--sm
|
||||
= check_box_tag("show_maybe_null", 1, false, data: { "no-autosubmit" => true, action: "change->attestation#toggleMaybeNull"})
|
||||
= label_tag "show_maybe_null", for: :show_maybe_null do
|
||||
Voir les champs facultatifs
|
||||
%span.hidden.fr-hint-text Un champ non rempli restera vide dans l’attestation.
|
||||
|
||||
%ul.fr-tags-group{ data: { category: category } }
|
||||
- tags.each do |tag|
|
||||
|
|
|
@ -34,7 +34,11 @@ module Administrateurs
|
|||
private
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
100
app/controllers/instructeurs/export_templates_controller.rb
Normal file
100
app/controllers/instructeurs/export_templates_controller.rb
Normal file
|
@ -0,0 +1,100 @@
|
|||
module Instructeurs
|
||||
class ExportTemplatesController < InstructeurController
|
||||
before_action :set_procedure
|
||||
before_action :set_groupe_instructeur, only: [:create, :update]
|
||||
before_action :set_export_template, only: [:edit, :update, :destroy]
|
||||
before_action :set_groupe_instructeurs
|
||||
before_action :set_all_pj
|
||||
|
||||
def new
|
||||
@export_template = ExportTemplate.new(kind: 'zip', groupe_instructeur: @groupe_instructeurs.first)
|
||||
@export_template.set_default_values
|
||||
end
|
||||
|
||||
def create
|
||||
@export_template = @groupe_instructeur.export_templates.build(export_template_params)
|
||||
@export_template.assign_pj_names(pj_params)
|
||||
if @export_template.save
|
||||
redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été créé"
|
||||
else
|
||||
flash[:alert] = @export_template.errors.full_messages
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@export_template.assign_attributes(export_template_params)
|
||||
@export_template.groupe_instructeur = @groupe_instructeur
|
||||
@export_template.assign_pj_names(pj_params)
|
||||
if @export_template.save
|
||||
redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été modifié"
|
||||
else
|
||||
flash[:alert] = @export_template.errors.full_messages
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @export_template.destroy
|
||||
redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été supprimé"
|
||||
else
|
||||
redirect_to exports_instructeur_procedure_path(procedure: @procedure), alert: "Le modèle d'export #{@export_template.name} n'a pu être supprimé"
|
||||
end
|
||||
end
|
||||
|
||||
def preview
|
||||
set_groupe_instructeur
|
||||
@export_template = @groupe_instructeur.export_templates.build(export_template_params)
|
||||
@export_template.assign_pj_names(pj_params)
|
||||
|
||||
@sample_dossier = @procedure.dossier_for_preview(current_instructeur)
|
||||
|
||||
render turbo_stream: turbo_stream.replace('preview', partial: 'preview', locals: { export_template: @export_template, procedure: @procedure, dossier: @sample_dossier })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def export_template_params
|
||||
params.require(:export_template).permit(*export_params)
|
||||
end
|
||||
|
||||
def set_procedure
|
||||
@procedure = current_instructeur.procedures.find params[:procedure_id]
|
||||
Sentry.configure_scope do |scope|
|
||||
scope.set_tags(procedure: @procedure.id)
|
||||
end
|
||||
end
|
||||
|
||||
def set_export_template
|
||||
@export_template = current_instructeur.export_templates.find(params[:id])
|
||||
end
|
||||
|
||||
def set_groupe_instructeur
|
||||
@groupe_instructeur = @procedure.groupe_instructeurs.find(params.require(:export_template)[:groupe_instructeur_id])
|
||||
end
|
||||
|
||||
def set_groupe_instructeurs
|
||||
@groupe_instructeurs = current_instructeur.groupe_instructeurs.where(procedure: @procedure)
|
||||
end
|
||||
|
||||
def set_all_pj
|
||||
@all_pj ||= @procedure.exportables_pieces_jointes
|
||||
end
|
||||
|
||||
def export_params
|
||||
[:name, :kind, :tiptap_default_dossier_directory, :tiptap_pdf_name]
|
||||
end
|
||||
|
||||
def pj_params
|
||||
@procedure = current_instructeur.procedures.find params[:procedure_id]
|
||||
pj_params = []
|
||||
@all_pj.each do |pj|
|
||||
pj_params << "tiptap_pj_#{pj.stable_id}".to_sym
|
||||
end
|
||||
params.require(:export_template).permit(*pj_params)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -245,6 +245,7 @@ module Instructeurs
|
|||
def exports
|
||||
@procedure = procedure
|
||||
@exports = Export.for_groupe_instructeurs(groupe_instructeur_ids).ante_chronological
|
||||
@export_templates = current_instructeur.export_templates_for(@procedure).includes(:groupe_instructeur)
|
||||
cookies.encrypted[cookies_export_key] = {
|
||||
value: DateTime.current,
|
||||
expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT
|
||||
|
@ -324,13 +325,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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
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)
|
||||
files = []
|
||||
DossierPreloader.new(dossiers).in_batches_with_block do |loaded_dossiers|
|
||||
files += pj_service.generate_dossiers_export(loaded_dossiers) + pj_service.liste_documents(loaded_dossiers)
|
||||
end
|
||||
|
||||
files
|
||||
end
|
||||
|
||||
def self.cleanup_list_from_dossier(files)
|
||||
|
|
|
@ -18,7 +18,7 @@ module Recovery
|
|||
etablissement: :exercices,
|
||||
revision: :procedure)
|
||||
@dossiers = DossierPreloader.new(dossier_with_data,
|
||||
includes_for_dossier: [:geo_areas, etablissement: :exercices],
|
||||
includes_for_champ: [:geo_areas, etablissement: :exercices],
|
||||
includes_for_etablissement: [:exercices]).all
|
||||
@file_path = file_path
|
||||
end
|
||||
|
|
|
@ -91,6 +91,10 @@ class Champ < ApplicationRecord
|
|||
parent_id.present?
|
||||
end
|
||||
|
||||
def stable_id_with_row
|
||||
[row_id, stable_id].compact
|
||||
end
|
||||
|
||||
# used for the `required` html attribute
|
||||
# check visibility to avoid hidden required input
|
||||
# which prevent the form from being sent.
|
||||
|
|
39
app/models/concerns/pieces_jointes_list_concern.rb
Normal file
39
app/models/concerns/pieces_jointes_list_concern.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
module PiecesJointesListConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
def public_wrapped_partionned_pjs
|
||||
pieces_jointes(public_only: true, wrap_with_parent: true)
|
||||
.partition { |(pj, _)| pj.condition.nil? }
|
||||
end
|
||||
|
||||
def exportables_pieces_jointes
|
||||
pieces_jointes(exclude_titre_identite: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pieces_jointes(
|
||||
exclude_titre_identite: false,
|
||||
public_only: false,
|
||||
wrap_with_parent: false
|
||||
)
|
||||
coordinates = active_revision.revision_types_de_champ
|
||||
.includes(:type_de_champ, revision_types_de_champ: :type_de_champ)
|
||||
|
||||
coordinates = coordinates.public_only if public_only
|
||||
|
||||
type_champ = ['piece_justificative']
|
||||
type_champ << 'titre_identite' if !exclude_titre_identite
|
||||
|
||||
coordinates = coordinates.where(types_de_champ: { type_champ: })
|
||||
|
||||
return coordinates.map(&:type_de_champ) if !wrap_with_parent
|
||||
|
||||
# we want pj in the form of [[pj1], [pj2, repetition], [pj3, repetition]]
|
||||
coordinates
|
||||
.map { |c| c.child? ? [c, c.parent] : [c] }
|
||||
.map { |a| a.map(&:type_de_champ) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -61,6 +61,15 @@ module TagsSubstitutionConcern
|
|||
end
|
||||
end
|
||||
|
||||
DOSSIER_ID_TAG = {
|
||||
id: 'dossier_number',
|
||||
label: 'numéro du dossier',
|
||||
libelle: 'numéro du dossier',
|
||||
description: '',
|
||||
lambda: -> (d) { d.id },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
}
|
||||
|
||||
DOSSIER_TAGS = [
|
||||
{
|
||||
id: 'dossier_motivation',
|
||||
|
@ -98,13 +107,6 @@ module TagsSubstitutionConcern
|
|||
lambda: -> (d) { d.procedure.libelle },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'dossier_number',
|
||||
libelle: 'numéro du dossier',
|
||||
description: '',
|
||||
target: :id,
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'dossier_service_name',
|
||||
libelle: 'nom du service',
|
||||
|
@ -112,7 +114,7 @@ module TagsSubstitutionConcern
|
|||
lambda: -> (d) { d.procedure.organisation_name || '' },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
}
|
||||
]
|
||||
].push(DOSSIER_ID_TAG)
|
||||
|
||||
DOSSIER_TAGS_FOR_MAIL = [
|
||||
{
|
||||
|
@ -152,21 +154,21 @@ module TagsSubstitutionConcern
|
|||
id: 'individual_gender',
|
||||
libelle: 'civilité',
|
||||
description: 'M., Mme',
|
||||
target: :gender,
|
||||
lambda: -> (d) { d.individual&.gender },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'individual_last_name',
|
||||
libelle: 'nom',
|
||||
description: "nom de l'usager",
|
||||
target: :nom,
|
||||
lambda: -> (d) { d.individual&.nom },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'individual_first_name',
|
||||
libelle: 'prénom',
|
||||
description: "prénom de l'usager",
|
||||
target: :prenom,
|
||||
lambda: -> (d) { d.individual&.prenom },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
}
|
||||
]
|
||||
|
@ -176,35 +178,35 @@ module TagsSubstitutionConcern
|
|||
id: 'entreprise_siren',
|
||||
libelle: 'SIREN',
|
||||
description: '',
|
||||
target: :siren,
|
||||
lambda: -> (d) { d.etablissement&.entreprise&.siren },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'entreprise_numero_tva_intracommunautaire',
|
||||
libelle: 'numéro de TVA intracommunautaire',
|
||||
description: '',
|
||||
target: :numero_tva_intracommunautaire,
|
||||
lambda: -> (d) { d.etablissement&.entreprise&.numero_tva_intracommunautaire },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'entreprise_siret_siege_social',
|
||||
libelle: 'SIRET du siège social',
|
||||
description: '',
|
||||
target: :siret_siege_social,
|
||||
lambda: -> (d) { d.etablissement&.entreprise&.siret_siege_social },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'entreprise_raison_sociale',
|
||||
libelle: 'raison sociale',
|
||||
description: '',
|
||||
target: :raison_sociale,
|
||||
lambda: -> (d) { d.etablissement&.entreprise&.raison_sociale },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
},
|
||||
{
|
||||
id: 'entreprise_adresse',
|
||||
libelle: 'adresse',
|
||||
description: '',
|
||||
target: :inline_adresse,
|
||||
lambda: -> (d) { d.etablissement&.entreprise&.inline_adresse },
|
||||
available_for_states: Dossier::SOUMIS
|
||||
}
|
||||
]
|
||||
|
@ -273,7 +275,7 @@ module TagsSubstitutionConcern
|
|||
used_tags_and_libelle_for(text).map { _1.first.nil? ? _1.second : _1.first }
|
||||
end
|
||||
|
||||
def tags_substitutions(tags_and_libelles, dossier, escape: true)
|
||||
def tags_substitutions(tags_and_libelles, dossier, escape: true, memoize: false)
|
||||
# NOTE:
|
||||
# - tags_and_libelles est un simple Set de couples (tag_id, libelle) (pas la même structure que dans replace_tags)
|
||||
# - dans `replace_tags`, on fait référence à des tags avec ou sans id, mais pas ici,
|
||||
|
@ -281,20 +283,20 @@ module TagsSubstitutionConcern
|
|||
|
||||
@escape_unsafe_tags = escape
|
||||
|
||||
flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |(tags, data), result|
|
||||
next if data.nil?
|
||||
|
||||
valid_tags = tags_for_dossier_state(tags)
|
||||
|
||||
valid_tags.each do |tag|
|
||||
result[tag[:id]] = [tag, data]
|
||||
end
|
||||
flat_tags = if memoize && @flat_tags.present?
|
||||
@flat_tags
|
||||
else
|
||||
available_tags(dossier)
|
||||
.flatten
|
||||
.then { tags_for_dossier_state(_1) }
|
||||
.index_by { _1[:id] }
|
||||
end
|
||||
|
||||
@flat_tags = flat_tags if memoize
|
||||
|
||||
tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions|
|
||||
substitutions[tag_id] = case flat_tags[tag_id]
|
||||
in tag, data
|
||||
replace_tag(tag, data)
|
||||
substitutions[tag_id] = if flat_tags[tag_id].present?
|
||||
replace_tag(flat_tags[tag_id], dossier)
|
||||
else # champ not in dossier, for example during preview on draft revision
|
||||
libelle
|
||||
end
|
||||
|
@ -370,8 +372,8 @@ module TagsSubstitutionConcern
|
|||
|
||||
tokens = parse_tags(text)
|
||||
|
||||
tags_and_datas = tags_and_datas_list(dossier).filter_map do |(tags, data)|
|
||||
data && [tags_for_dossier_state(tags).index_by { _1[:id] }, data]
|
||||
tags_and_datas = available_tags(dossier).filter_map do |tags|
|
||||
dossier && [tags_for_dossier_state(tags).index_by { _1[:id] }, dossier]
|
||||
end
|
||||
|
||||
tags_and_datas.reduce(tokens) do |tokens, (tags, data)|
|
||||
|
@ -397,12 +399,8 @@ module TagsSubstitutionConcern
|
|||
end.join('')
|
||||
end
|
||||
|
||||
def replace_tag(tag, data)
|
||||
value = if tag.key?(:target)
|
||||
data.public_send(tag[:target])
|
||||
else
|
||||
instance_exec(data, &tag[:lambda])
|
||||
end
|
||||
def replace_tag(tag, dossier)
|
||||
value = instance_exec(dossier, &tag[:lambda])
|
||||
|
||||
if escape_unsafe_tags? && tag.fetch(:escapable, true)
|
||||
escape_once(value)
|
||||
|
@ -449,14 +447,14 @@ module TagsSubstitutionConcern
|
|||
end
|
||||
end
|
||||
|
||||
def tags_and_datas_list(dossier)
|
||||
def available_tags(dossier)
|
||||
[
|
||||
[champ_public_tags(dossier:), dossier],
|
||||
[champ_private_tags(dossier:), dossier],
|
||||
[dossier_tags, dossier],
|
||||
[ROUTAGE_TAGS, dossier],
|
||||
[INDIVIDUAL_TAGS, dossier.individual],
|
||||
[ENTREPRISE_TAGS, dossier.etablissement&.entreprise]
|
||||
champ_public_tags(dossier:),
|
||||
champ_private_tags(dossier:),
|
||||
dossier_tags,
|
||||
ROUTAGE_TAGS,
|
||||
INDIVIDUAL_TAGS,
|
||||
ENTREPRISE_TAGS
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
class DossierPreloader
|
||||
DEFAULT_BATCH_SIZE = 2000
|
||||
|
||||
def initialize(dossiers, includes_for_dossier: [], includes_for_etablissement: [])
|
||||
def initialize(dossiers, includes_for_champ: [], includes_for_etablissement: [])
|
||||
@dossiers = dossiers
|
||||
@includes_for_etablissement = includes_for_etablissement
|
||||
@includes_for_dossier = includes_for_dossier
|
||||
@includes_for_champ = includes_for_champ
|
||||
end
|
||||
|
||||
def in_batches(size = DEFAULT_BATCH_SIZE)
|
||||
|
@ -13,6 +13,16 @@ class DossierPreloader
|
|||
dossiers
|
||||
end
|
||||
|
||||
def in_batches_with_block(size = DEFAULT_BATCH_SIZE, &block)
|
||||
@dossiers.in_batches(of: size) do |batch|
|
||||
data = Dossier.where(id: batch.ids).includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert], revision: :revision_types_de_champ)
|
||||
|
||||
dossiers = data.to_a
|
||||
load_dossiers(dossiers)
|
||||
yield(dossiers)
|
||||
end
|
||||
end
|
||||
|
||||
def all(pj_template: false)
|
||||
dossiers = @dossiers.to_a
|
||||
load_dossiers(dossiers, pj_template:)
|
||||
|
@ -37,7 +47,7 @@ class DossierPreloader
|
|||
end
|
||||
|
||||
def load_dossiers(dossiers, pj_template: false)
|
||||
to_include = @includes_for_dossier.dup
|
||||
to_include = @includes_for_champ.dup
|
||||
to_include << [piece_justificative_file_attachments: :blob]
|
||||
|
||||
if pj_template
|
||||
|
|
|
@ -31,6 +31,7 @@ class Export < ApplicationRecord
|
|||
belongs_to :procedure_presentation, optional: true
|
||||
belongs_to :instructeur, optional: true
|
||||
belongs_to :user_profile, polymorphic: true, optional: true
|
||||
belongs_to :export_template, optional: true
|
||||
|
||||
has_one_attached :file
|
||||
|
||||
|
@ -66,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)
|
||||
|
@ -147,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
|
||||
|
|
155
app/models/export_template.rb
Normal file
155
app/models/export_template.rb
Normal file
|
@ -0,0 +1,155 @@
|
|||
class ExportTemplate < ApplicationRecord
|
||||
include TagsSubstitutionConcern
|
||||
|
||||
belongs_to :groupe_instructeur
|
||||
has_one :procedure, through: :groupe_instructeur
|
||||
has_many :exports, dependent: :nullify
|
||||
validates_with ExportTemplateValidator
|
||||
|
||||
DOSSIER_STATE = Dossier.states.fetch(:en_construction)
|
||||
|
||||
def set_default_values
|
||||
content["default_dossier_directory"] = tiptap_json("dossier-")
|
||||
content["pdf_name"] = tiptap_json("export_")
|
||||
|
||||
content["pjs"] = []
|
||||
procedure.exportables_pieces_jointes.each do |pj|
|
||||
content["pjs"] << { "stable_id" => pj.stable_id.to_s, "path" => tiptap_json("#{pj.libelle.parameterize}-") }
|
||||
end
|
||||
end
|
||||
|
||||
def tiptap_default_dossier_directory=(body)
|
||||
self.content["default_dossier_directory"] = JSON.parse(body)
|
||||
end
|
||||
|
||||
def tiptap_default_dossier_directory
|
||||
tiptap_content("default_dossier_directory")
|
||||
end
|
||||
|
||||
def tiptap_pdf_name=(body)
|
||||
self.content["pdf_name"] = JSON.parse(body)
|
||||
end
|
||||
|
||||
def tiptap_pdf_name
|
||||
tiptap_content("pdf_name")
|
||||
end
|
||||
|
||||
def content_for_pj(pj)
|
||||
content_for_pj_id(pj.stable_id)&.to_json
|
||||
end
|
||||
|
||||
def assign_pj_names(pj_params)
|
||||
self.content["pjs"] = []
|
||||
pj_params.each do |pj_param|
|
||||
self.content["pjs"] << { stable_id: pj_param[0].delete_prefix("tiptap_pj_"), path: JSON.parse(pj_param[1]) }
|
||||
end
|
||||
end
|
||||
|
||||
def attachment_and_path(dossier, attachment, index: 0, row_index: nil, champ: nil)
|
||||
[
|
||||
attachment,
|
||||
path(dossier, attachment, index, row_index, champ)
|
||||
]
|
||||
end
|
||||
|
||||
def tiptap_convert(dossier, param)
|
||||
if content[param]["content"]&.first&.[]("content")
|
||||
render_attributes_for(content[param], dossier)
|
||||
end
|
||||
end
|
||||
|
||||
def tiptap_convert_pj(dossier, pj_stable_id, attachment = nil)
|
||||
if content_for_pj_id(pj_stable_id)["content"]&.first&.[]("content")
|
||||
render_attributes_for(content_for_pj_id(pj_stable_id), dossier, attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def render_attributes_for(content_for, dossier, attachment = nil)
|
||||
tiptap = TiptapService.new
|
||||
used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys)
|
||||
substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true)
|
||||
substitutions['original-filename'] = attachment.filename.base if attachment
|
||||
tiptap.to_path(content_for.deep_symbolize_keys, substitutions)
|
||||
end
|
||||
|
||||
def specific_tags
|
||||
tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten
|
||||
end
|
||||
|
||||
def tags_for_pj
|
||||
specific_tags.push({
|
||||
libelle: 'nom original du fichier',
|
||||
id: 'original-filename',
|
||||
maybe_null: false
|
||||
})
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tiptap_content(key)
|
||||
content[key]&.to_json
|
||||
end
|
||||
|
||||
def tiptap_json(prefix)
|
||||
{
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => prefix, "type" => "text" }, { "type" => "mention", "attrs" => DOSSIER_ID_TAG.stringify_keys }] }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def content_for_pj_id(stable_id)
|
||||
content_for_stable_id = content["pjs"].find { _1.symbolize_keys[:stable_id] == stable_id.to_s }
|
||||
content_for_stable_id.symbolize_keys.fetch(:path)
|
||||
end
|
||||
|
||||
def folder(dossier)
|
||||
render_attributes_for(content["default_dossier_directory"], dossier)
|
||||
end
|
||||
|
||||
def export_path(dossier)
|
||||
File.join(folder(dossier), export_filename(dossier))
|
||||
end
|
||||
|
||||
def export_filename(dossier)
|
||||
"#{render_attributes_for(content["pdf_name"], dossier)}.pdf"
|
||||
end
|
||||
|
||||
def path(dossier, attachment, index, row_index, champ)
|
||||
if attachment.name == 'pdf_export_for_instructeur'
|
||||
return export_path(dossier)
|
||||
end
|
||||
|
||||
dir_path = case attachment.record_type
|
||||
when 'Dossier'
|
||||
'dossier'
|
||||
when 'Commentaire'
|
||||
'messagerie'
|
||||
when 'Avis'
|
||||
'avis'
|
||||
else
|
||||
# for attachment
|
||||
return attachment_path(dossier, attachment, index, row_index, champ)
|
||||
end
|
||||
|
||||
File.join(folder(dossier), dir_path, attachment.filename.to_s)
|
||||
end
|
||||
|
||||
def attachment_path(dossier, attachment, index, row_index, champ)
|
||||
stable_id = champ.stable_id
|
||||
tiptap_pj = content["pjs"].find { |pj| pj["stable_id"] == stable_id.to_s }
|
||||
if tiptap_pj
|
||||
File.join(folder(dossier), tiptap_convert_pj(dossier, stable_id, attachment) + suffix(attachment, index, row_index))
|
||||
else
|
||||
File.join(folder(dossier), "erreur_renommage", attachment.filename.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def suffix(attachment, index, row_index)
|
||||
suffix = "-#{index + 1}"
|
||||
suffix += "-#{row_index + 1}" if row_index.present?
|
||||
|
||||
suffix + attachment.filename.extension_with_delimiter
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ class GroupeInstructeur < ApplicationRecord
|
|||
has_many :batch_operations, through: :dossiers, source: :batch_operations
|
||||
has_many :assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :groupe_instructeur
|
||||
has_many :previous_assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :previous_groupe_instructeur
|
||||
has_many :export_templates
|
||||
has_and_belongs_to_many :exports, dependent: :destroy
|
||||
|
||||
has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur
|
||||
|
|
|
@ -14,6 +14,7 @@ class Instructeur < ApplicationRecord
|
|||
has_many :batch_operations, dependent: :nullify
|
||||
has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo', inverse_of: :instructeur
|
||||
has_many :groupe_instructeur_with_email_notifications, through: :assign_to_with_email_notifications, source: :groupe_instructeur
|
||||
has_many :export_templates, through: :groupe_instructeurs
|
||||
|
||||
has_many :commentaires, inverse_of: :instructeur, dependent: :nullify
|
||||
has_many :dossiers, -> { state_not_brouillon }, through: :unordered_groupe_instructeurs
|
||||
|
@ -302,6 +303,10 @@ class Instructeur < ApplicationRecord
|
|||
agent_connect_information.order(updated_at: :desc).first
|
||||
end
|
||||
|
||||
def export_templates_for(procedure)
|
||||
procedure.export_templates.where(groupe_instructeur: groupe_instructeurs).order(:name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def annotations_hash(demande, annotations_privees, avis, messagerie)
|
||||
|
|
|
@ -5,6 +5,7 @@ class Procedure < ApplicationRecord
|
|||
include ProcedureGroupeInstructeurAPIHackConcern
|
||||
include ProcedureSVASVRConcern
|
||||
include ProcedureChorusConcern
|
||||
include PiecesJointesListConcern
|
||||
|
||||
include Discard::Model
|
||||
self.discard_column = :hidden_at
|
||||
|
@ -153,6 +154,7 @@ class Procedure < ApplicationRecord
|
|||
has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! }
|
||||
has_many :groupe_instructeurs, -> { order(:label) }, inverse_of: :procedure, dependent: :destroy
|
||||
has_many :instructeurs, through: :groupe_instructeurs
|
||||
has_many :export_templates, through: :groupe_instructeurs
|
||||
|
||||
has_many :active_groupe_instructeurs, -> { active }, class_name: 'GroupeInstructeur', inverse_of: false
|
||||
has_many :closed_groupe_instructeurs, -> { closed }, class_name: 'GroupeInstructeur', inverse_of: false
|
||||
|
@ -981,22 +983,6 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def pieces_jointes_list?
|
||||
pieces_jointes_list_without_conditionnal.present? || pieces_jointes_list_with_conditionnal.present?
|
||||
end
|
||||
|
||||
def pieces_jointes_list_without_conditionnal
|
||||
pieces_jointes_list do |base_scope|
|
||||
base_scope.where(types_de_champ: { condition: nil })
|
||||
end
|
||||
end
|
||||
|
||||
def pieces_jointes_list_with_conditionnal
|
||||
pieces_jointes_list do |base_scope|
|
||||
base_scope.where.not(types_de_champ: { condition: nil })
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_routing
|
||||
update!(routing_enabled: self.groupe_instructeurs.active.many?)
|
||||
end
|
||||
|
@ -1024,22 +1010,6 @@ class Procedure < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def pieces_jointes_list
|
||||
scope = yield active_revision.revision_types_de_champ_public
|
||||
.includes(:type_de_champ, revision_types_de_champ: :type_de_champ)
|
||||
.where(types_de_champ: { type_champ: ['repetition', 'piece_justificative', 'titre_identite'] })
|
||||
|
||||
scope.each_with_object([]) do |rtdc, list|
|
||||
if rtdc.type_de_champ.repetition?
|
||||
rtdc.revision_types_de_champ.each do |rtdc_in_repetition|
|
||||
list << [rtdc_in_repetition.type_de_champ, rtdc.type_de_champ] if rtdc_in_repetition.type_de_champ.piece_justificative?
|
||||
end
|
||||
else
|
||||
list << [rtdc.type_de_champ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_auto_archive_on_in_the_future
|
||||
return if auto_archive_on.nil?
|
||||
return if auto_archive_on.future?
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
class DownloadableFileService
|
||||
ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' }
|
||||
EXPORT_DIRNAME = 'export'
|
||||
|
||||
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)
|
||||
export_dir = File.join(tmp_dir, EXPORT_DIRNAME)
|
||||
zip_path = File.join(ARCHIVE_CREATION_DIR, "#{filename}.zip")
|
||||
|
||||
begin
|
||||
|
@ -15,7 +16,7 @@ class DownloadableFileService
|
|||
|
||||
Dir.chdir(tmp_dir) do
|
||||
File.delete(zip_path) if File.exist?(zip_path)
|
||||
system 'zip', '-0', '-r', zip_path, filename
|
||||
system 'zip', '-0', '-r', zip_path, EXPORT_DIRNAME
|
||||
end
|
||||
yield(zip_path)
|
||||
ensure
|
||||
|
|
|
@ -1,27 +1,24 @@
|
|||
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)
|
||||
bill_ids = []
|
||||
|
||||
docs = dossiers.in_batches.flat_map do |batch|
|
||||
pjs = pjs_for_champs(batch) +
|
||||
pjs_for_commentaires(batch) +
|
||||
pjs_for_dossier(batch) +
|
||||
pjs_for_avis(batch)
|
||||
docs = pjs_for_champs(dossiers) +
|
||||
pjs_for_commentaires(dossiers) +
|
||||
pjs_for_dossier(dossiers) +
|
||||
pjs_for_avis(dossiers)
|
||||
|
||||
if liste_documents_allows?(:with_bills)
|
||||
# some bills are shared among operations
|
||||
# so first, all the bill_ids are fetched
|
||||
operation_logs, some_bill_ids = operation_logs_and_signature_ids(batch)
|
||||
if liste_documents_allows?(:with_bills)
|
||||
# some bills are shared among operations
|
||||
# so first, all the bill_ids are fetched
|
||||
operation_logs, some_bill_ids = operation_logs_and_signature_ids(dossiers)
|
||||
|
||||
pjs += operation_logs
|
||||
bill_ids += some_bill_ids
|
||||
end
|
||||
|
||||
pjs
|
||||
docs += operation_logs
|
||||
bill_ids += some_bill_ids
|
||||
end
|
||||
|
||||
if liste_documents_allows?(:with_bills)
|
||||
|
@ -32,14 +29,12 @@ class PiecesJustificativesService
|
|||
docs
|
||||
end
|
||||
|
||||
def generate_dossiers_export(dossiers)
|
||||
def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s
|
||||
return [] if dossiers.empty?
|
||||
|
||||
pdfs = []
|
||||
|
||||
procedure = dossiers.first.procedure
|
||||
dossiers = dossiers.includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert])
|
||||
dossiers = DossierPreloader.new(dossiers).in_batches
|
||||
dossiers.each do |dossier|
|
||||
dossier.association(:procedure).target = procedure
|
||||
|
||||
|
@ -49,7 +44,6 @@ class PiecesJustificativesService
|
|||
acls: acl_for_dossier_export(procedure),
|
||||
dossier: dossier
|
||||
})
|
||||
|
||||
a = ActiveStorage::FakeAttachment.new(
|
||||
file: StringIO.new(pdf),
|
||||
filename: "export-#{dossier.id}.pdf",
|
||||
|
@ -58,7 +52,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
|
||||
|
@ -137,26 +135,25 @@ class PiecesJustificativesService
|
|||
end
|
||||
|
||||
def pjs_for_champs(dossiers)
|
||||
champs = Champ
|
||||
.joins(:piece_justificative_file_attachments)
|
||||
.where(type: "Champs::PieceJustificativeChamp", dossier: dossiers)
|
||||
champs = dossiers.flat_map(&:champs).filter { _1.type == "Champs::PieceJustificativeChamp" }
|
||||
|
||||
if !liste_documents_allows?(:with_champs_private)
|
||||
champs = champs.where(private: false)
|
||||
champs = champs.reject(&:private?)
|
||||
end
|
||||
|
||||
champ_id_dossier_id = champs
|
||||
.pluck(:id, :dossier_id)
|
||||
.to_h
|
||||
champs_id_row_index = compute_champ_id_row_index(champs)
|
||||
|
||||
ActiveStorage::Attachment
|
||||
.includes(:blob)
|
||||
.where(record_type: "Champ", record_id: champ_id_dossier_id.keys)
|
||||
.filter { |a| safe_attachment(a) }
|
||||
.map do |a|
|
||||
dossier_id = champ_id_dossier_id[a.record_id]
|
||||
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
|
||||
champs.flat_map do |champ|
|
||||
champ.piece_justificative_file_attachments.filter { |a| safe_attachment(a) }.map.with_index do |attachment, index|
|
||||
row_index = champs_id_row_index[champ.id]
|
||||
|
||||
if @export_template
|
||||
@export_template.attachment_and_path(champ.dossier, attachment, index:, row_index:, champ:)
|
||||
else
|
||||
ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pjs_for_commentaires(dossiers)
|
||||
|
@ -300,4 +297,26 @@ class PiecesJustificativesService
|
|||
.blob
|
||||
.virus_scan_result == ActiveStorage::VirusScanner::SAFE
|
||||
end
|
||||
|
||||
# given
|
||||
# repet_0 (stable_id: r0)
|
||||
# # row_0
|
||||
# # # pj_champ_0 (stable_id: 0)
|
||||
# # row_1
|
||||
# # # pj_champ_1 (stable_id: 0)
|
||||
# repet_1 (stable_id: r1)
|
||||
# # row_0
|
||||
# # # pj_champ_2 (stable_id: 1)
|
||||
# # # pj_champ_3 (stable_id: 2)
|
||||
# # row_1
|
||||
# # # pj_champ_4 (stable_id: 1)
|
||||
# # # pj_champ_5 (stable_id: 2)
|
||||
# it returns { pj_0.id => 0, pj_1.id => 1, pj_2.id => 0, pj_3.id => 0, pj_4.id => 1, pj_5.id => 1 }
|
||||
def compute_champ_id_row_index(champs)
|
||||
champs.filter(&:child?).group_by(&:dossier_id).values.each_with_object({}) do |children_for_dossier, hash|
|
||||
children_for_dossier.group_by(&:stable_id).values.each do |champs_for_stable_id|
|
||||
champs_for_stable_id.sort_by(&:row_id).each.with_index { |c, index| hash[c.id] = index }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,12 @@ class TiptapService
|
|||
children(node[:content], substitutions, 0)
|
||||
end
|
||||
|
||||
def to_path(node, substitutions = {})
|
||||
return '' if node.nil?
|
||||
|
||||
children_path(node[:content], substitutions)
|
||||
end
|
||||
|
||||
# NOTE: node must be deep symbolized keys
|
||||
def used_tags_and_libelle_for(node, tags = Set.new)
|
||||
case node
|
||||
|
@ -25,6 +31,21 @@ class TiptapService
|
|||
@body_started = false
|
||||
end
|
||||
|
||||
def children_path(content, substitutions)
|
||||
content.map { node_to_path(_1, substitutions) }.join
|
||||
end
|
||||
|
||||
def node_to_path(node, substitutions)
|
||||
case node
|
||||
in type: 'paragraph', content:
|
||||
children_path(content, substitutions)
|
||||
in type: 'text', text:, **rest
|
||||
text.strip
|
||||
in type: 'mention', attrs: { id: }, **rest
|
||||
substitutions.fetch(id) { "--#{id}--" }
|
||||
end
|
||||
end
|
||||
|
||||
def children(content, substitutions, level)
|
||||
content.map { node_to_html(_1, substitutions, level) }.join
|
||||
end
|
||||
|
|
54
app/validators/export_template_validator.rb
Normal file
54
app/validators/export_template_validator.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
class ExportTemplateValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
validate_default_dossier_directory(record)
|
||||
validate_pdf_name(record)
|
||||
validate_pjs(record)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_default_dossier_directory(record)
|
||||
mention = attribute_content_mention(record, :default_dossier_directory)
|
||||
if mention&.fetch("id", nil) != "dossier_number"
|
||||
record.errors.add :tiptap_default_dossier_directory, :dossier_number_mandatory
|
||||
end
|
||||
end
|
||||
|
||||
def validate_pdf_name(record)
|
||||
if attribute_content_text(record, :pdf_name).blank? && attribute_content_mention(record, :pdf_name).blank?
|
||||
record.errors.add :tiptap_pdf_name, :blank
|
||||
end
|
||||
end
|
||||
|
||||
def attribute_content_text(record, attribute)
|
||||
attribute_content(record, attribute)&.find { |elem| elem["type"] == "text" }&.fetch("text", nil)
|
||||
end
|
||||
|
||||
def attribute_content_mention(record, attribute)
|
||||
attribute_content(record, attribute)&.find { |elem| elem["type"] == "mention" }&.fetch("attrs", nil)
|
||||
end
|
||||
|
||||
def attribute_content(record, attribute)
|
||||
content = record.content[attribute.to_s]&.fetch("content", nil)
|
||||
if content.is_a?(Array)
|
||||
content.first&.fetch("content", nil)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_pjs(record)
|
||||
record.content["pjs"]&.each do |pj|
|
||||
pj_sym = pj.symbolize_keys
|
||||
libelle = record.groupe_instructeur.procedure.exportables_pieces_jointes.find { _1.stable_id.to_s == pj_sym[:stable_id] }&.libelle&.to_sym
|
||||
validate_content(record, pj_sym[:path], libelle)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_content(record, attribute_content, attribute)
|
||||
if attribute_content.nil? || attribute_content["content"].nil? ||
|
||||
attribute_content["content"].first.nil? ||
|
||||
attribute_content["content"].first["content"].nil? ||
|
||||
(attribute_content["content"].first["content"].find { |elem| elem["text"].blank? } && attribute_content["content"].first["content"].find { |elem| elem["type"] == "mention" }["attrs"].blank?)
|
||||
record.errors.add attribute, I18n.t(:blank, scope: 'errors.messages')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -77,7 +77,7 @@
|
|||
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
|
||||
= label
|
||||
|
||||
#editor.editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} }
|
||||
#editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} }
|
||||
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
|
||||
|
||||
.fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
|
||||
|
|
78
app/views/instructeurs/export_templates/_form.html.haml
Normal file
78
app/views/instructeurs/export_templates/_form.html.haml
Normal file
|
@ -0,0 +1,78 @@
|
|||
#export_template-edit.fr-my-4w
|
||||
.fr-mb-6w
|
||||
= render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c|
|
||||
- c.with_body do
|
||||
Cette page permet d'éditer un modèle d'export et ainsi personnaliser le contenu des exports (pour l'instant,
|
||||
uniquement au format zip). Ainsi, vous pouvez notamment normaliser le nom des pièces jointes.
|
||||
Essayez-le et donnez-nous votre avis
|
||||
en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}.
|
||||
|
||||
.fr-grid-row.fr-grid-row--gutters
|
||||
.fr-col-12.fr-col-md-8
|
||||
= form_with url: form_url, model: @export_template, local: true, data: { turbo: 'true', controller: 'autosubmit' } do |f|
|
||||
|
||||
= render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
|
||||
|
||||
- if groupe_instructeurs.many?
|
||||
.fr-input-group
|
||||
= f.label :groupe_instructeur_id, class: 'fr-label' do
|
||||
= f.object.class.human_attribute_name(:groupe_instructeur_id)
|
||||
= render EditableChamp::AsteriskMandatoryComponent.new
|
||||
%span.fr-hint-text
|
||||
Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ?
|
||||
= f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select'
|
||||
- else
|
||||
= f.hidden_field :groupe_instructeur_id
|
||||
|
||||
= f.hidden_field :kind
|
||||
|
||||
.fr-input-group{ data: { controller: 'tiptap' } }
|
||||
= f.label :tiptap_default_dossier_directory, class: "fr-label" do
|
||||
= f.object.class.human_attribute_name(:tiptap_default_dossier_directory)
|
||||
= render EditableChamp::AsteriskMandatoryComponent.new
|
||||
%span.fr-hint-text
|
||||
= t('activerecord.attributes.export_template.hints.tiptap_default_dossier_directory')
|
||||
|
||||
.tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
|
||||
= f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input' }
|
||||
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags })
|
||||
|
||||
.fr-input-group{ data: { controller: 'tiptap' } }
|
||||
= f.label :tiptap_pdf_name, class: "fr-label" do
|
||||
= f.object.class.human_attribute_name(:tiptap_pdf_name)
|
||||
= render EditableChamp::AsteriskMandatoryComponent.new
|
||||
%span.fr-hint-text
|
||||
= t('activerecord.attributes.export_template.hints.tiptap_pdf_name')
|
||||
.tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
|
||||
= f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input' }
|
||||
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags })
|
||||
|
||||
- if @all_pj.any?
|
||||
%h3 Pieces justificatives
|
||||
|
||||
.fr-highlight
|
||||
%p.fr-text--sm
|
||||
N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes.
|
||||
|
||||
- @all_pj.each do |pj|
|
||||
.fr-input-group{ data: { controller: 'tiptap' } }
|
||||
= label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label"
|
||||
.tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
|
||||
= hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input' }
|
||||
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.tags_for_pj })
|
||||
|
||||
.fixed-footer
|
||||
.fr-container
|
||||
%ul.fr-btns-group.fr-btns-group--inline-md
|
||||
%li
|
||||
%input.hidden{ type: 'submit', formaction: preview_instructeur_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'get' }
|
||||
= f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' }
|
||||
%li
|
||||
= link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary"
|
||||
- if @export_template.persisted?
|
||||
%li
|
||||
= link_to "Supprimer", instructeur_export_template_path(@export_template, procedure_id: @procedure.id), method: :delete, data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"}, class: "fr-btn fr-btn--secondary"
|
||||
- sample_dossier = @procedure.dossier_for_preview(current_instructeur)
|
||||
- if sample_dossier
|
||||
.fr-col-12.fr-col-md-4.fr-background-alt--blue-france
|
||||
= render partial: 'preview', locals: { dossier: sample_dossier, export_template: @export_template, procedure: @procedure }
|
17
app/views/instructeurs/export_templates/_preview.html.haml
Normal file
17
app/views/instructeurs/export_templates/_preview.html.haml
Normal file
|
@ -0,0 +1,17 @@
|
|||
#preview.export-template-preview.fr-p-2w.sticky--top
|
||||
%h2.fr-h4 Aperçu
|
||||
%ul.tree.fr-text--sm
|
||||
%li #{DownloadableFileService::EXPORT_DIRNAME}/
|
||||
%li
|
||||
%ul
|
||||
%li
|
||||
%span#preview_default_dossier_directory #{export_template.tiptap_convert(dossier, "default_dossier_directory")}/
|
||||
%ul
|
||||
%li#preview_pdf_name #{export_template.tiptap_convert(dossier, "pdf_name")}.pdf
|
||||
- procedure.exportables_pieces_jointes.each do |pj|
|
||||
%li{ id: "preview_pj_#{pj.stable_id}" } #{export_template.tiptap_convert_pj(dossier, pj.stable_id)}-1.jpg
|
||||
%ul
|
||||
%li
|
||||
%span messagerie/
|
||||
%ul
|
||||
%li un-autre-fichier.png
|
7
app/views/instructeurs/export_templates/edit.html.haml
Normal file
7
app/views/instructeurs/export_templates/edit.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
|
||||
[t('.title')]] }
|
||||
.fr-container
|
||||
%h1 Mise à jour modèle d'export
|
||||
|
||||
= render partial: 'form', locals: { form_url: instructeur_export_template_path(@procedure, @export_template), groupe_instructeurs: @groupe_instructeurs }
|
6
app/views/instructeurs/export_templates/new.html.haml
Normal file
6
app/views/instructeurs/export_templates/new.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
|
||||
[t('.title')]] }
|
||||
.fr-container
|
||||
%h1 Nouveau modèle d'export
|
||||
= render partial: 'form', locals: { form_url: instructeur_export_templates_path, groupe_instructeurs: @groupe_instructeurs }
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
.procedure-actions
|
||||
- if @can_download_dossiers
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
.fr-container.flex= render partial: "tabs", locals: { procedure: @procedure,
|
||||
statut: @statut,
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
- if @can_download_dossiers
|
||||
- if @statut.nil?
|
||||
= turbo_stream.update_all '.procedure-actions' do
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path))
|
||||
- else
|
||||
= turbo_stream.update_all '.dossiers-export' do
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
= turbo_stream.update "last-export-alert" do
|
||||
= render partial: "last_export_alert", locals: { export: @last_export, statut: @statut }
|
||||
|
|
|
@ -22,3 +22,25 @@
|
|||
|
||||
- else
|
||||
= t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
|
||||
|
||||
- if feature_enabled?(:export_template)
|
||||
%h2.fr-mb-1w.fr-mt-8w
|
||||
Liste des modèles d'export
|
||||
%p.fr-hint-text
|
||||
Un modèle d'export permet de personnaliser le nom des fichiers (pour un export au format Zip)
|
||||
- if @export_templates.any?
|
||||
.fr-table.fr-table--no-caption.fr-mt-5w
|
||||
%table
|
||||
%thead
|
||||
%tr
|
||||
%th{ scope: 'col' } Nom du modèle
|
||||
%th{ scope: 'col' }= "Groupe instructeur" if @procedure.groupe_instructeurs.many?
|
||||
%tbody
|
||||
- @export_templates.each do |export_template|
|
||||
%tr
|
||||
%td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id)
|
||||
%td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many?
|
||||
|
||||
%p
|
||||
= link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do
|
||||
Ajouter un modèle d'export
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
.procedure-actions
|
||||
- if @can_download_dossiers
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
.fr-container.flex= render partial: "tabs", locals: { procedure: @procedure,
|
||||
statut: @statut,
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
- if @dossiers_count > 0
|
||||
%span.dossiers-export
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
|
||||
= render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path))
|
||||
|
||||
- if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0
|
||||
= render partial: "dossiers_filter_tags", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut }
|
||||
|
|
|
@ -48,21 +48,23 @@
|
|||
#accordion-116.fr-collapse
|
||||
= h render SimpleFormatComponent.new(procedure.description_pj, allow_a: true)
|
||||
|
||||
- elsif procedure.pieces_jointes_list?
|
||||
%section.fr-accordion.pieces_jointes
|
||||
%h2.fr-accordion__title
|
||||
%button.fr-accordion__btn{ "aria-controls" => "accordion-116", "aria-expanded" => "false" }
|
||||
= t('shared.procedure_description.pieces_jointes')
|
||||
#accordion-116.fr-collapse
|
||||
- if procedure.pieces_jointes_list_without_conditionnal.present?
|
||||
%ul
|
||||
= render partial: "shared/procedure_pieces_jointes_list", collection: procedure.pieces_jointes_list_without_conditionnal, as: :pj
|
||||
- else
|
||||
- pj_without_condition, pj_with_condition = procedure.public_wrapped_partionned_pjs
|
||||
- if pj_without_condition.present? || pj_with_condition.present?
|
||||
%section.fr-accordion.pieces_jointes
|
||||
%h2.fr-accordion__title
|
||||
%button.fr-accordion__btn{ "aria-controls" => "accordion-116", "aria-expanded" => "false" }
|
||||
= t('shared.procedure_description.pieces_jointes')
|
||||
#accordion-116.fr-collapse
|
||||
- if pj_without_condition.present?
|
||||
%ul
|
||||
= render partial: "shared/procedure_pieces_jointes_list", collection: pj_without_condition, as: :pj
|
||||
|
||||
- if procedure.pieces_jointes_list_with_conditionnal.present?
|
||||
%h3.fr-text--sm.fr-mb-0.fr-mt-2w
|
||||
= t('shared.procedure_description.pieces_jointes_conditionnal_list_title')
|
||||
%ul
|
||||
= render partial: "shared/procedure_pieces_jointes_list", collection: procedure.pieces_jointes_list_with_conditionnal, as: :pj
|
||||
- if pj_with_condition.present?
|
||||
%h3.fr-text--sm.fr-mb-0.fr-mt-2w
|
||||
= t('shared.procedure_description.pieces_jointes_conditionnal_list_title')
|
||||
%ul
|
||||
= render partial: "shared/procedure_pieces_jointes_list", collection: pj_with_condition, as: :pj
|
||||
|
||||
- estimated_delay_component = Procedure::EstimatedDelayComponent.new(procedure: procedure)
|
||||
- if estimated_delay_component.render?
|
||||
|
|
|
@ -25,6 +25,7 @@ features = [
|
|||
:dossier_pdf_vide,
|
||||
:engagement_juridique_type_de_champ,
|
||||
:export_order_by_revision,
|
||||
:export_template,
|
||||
:expression_reguliere_type_de_champ,
|
||||
:gallery_demande,
|
||||
:groupe_instructeur_api_hack,
|
||||
|
|
17
config/locales/models/export_templates/en.yml
Normal file
17
config/locales/models/export_templates/en.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
en:
|
||||
activerecord:
|
||||
models:
|
||||
export_template: Export template
|
||||
attributes:
|
||||
export_template:
|
||||
hints:
|
||||
name: "The name will be visible by you and the other instructors"
|
||||
tiptap_default_dossier_directory: "How would you like to name the directory containing the documents of a folder?"
|
||||
tiptap_pdf_name: "How would you like to name the pdf file containing all the user's answers?"
|
||||
name: "Template's name"
|
||||
tiptap_default_dossier_directory: "Directory's name for pdf format"
|
||||
tiptap_pdf_name: "Export's filename"
|
||||
errors:
|
||||
models:
|
||||
export_template:
|
||||
dossier_number_mandatory: "must contain dossier's number"
|
17
config/locales/models/export_templates/fr.yml
Normal file
17
config/locales/models/export_templates/fr.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
fr:
|
||||
activerecord:
|
||||
models:
|
||||
export_template: "Modèle d'export"
|
||||
attributes:
|
||||
export_template:
|
||||
hints:
|
||||
name: "Le nom sera visible par vous et les autres instructeurs pour générer un export"
|
||||
tiptap_default_dossier_directory: "Comment souhaitez-vous nommer le répertoire contenant les documents d'un dossier ?"
|
||||
tiptap_pdf_name: "Comment souhaitez-vous nommer le fichier pdf qui contient toutes les réponses de l'usager ?"
|
||||
name: "Nom du modèle"
|
||||
tiptap_default_dossier_directory: Nom du répertoire
|
||||
tiptap_pdf_name: "Nom du dossier au format pdf"
|
||||
errors:
|
||||
models:
|
||||
export_template:
|
||||
dossier_number_mandatory: doit contenir le numéro du dossier
|
|
@ -0,0 +1,8 @@
|
|||
fr:
|
||||
instructeurs:
|
||||
export_templates:
|
||||
new:
|
||||
title: Nouveau modèle d'export
|
||||
edit:
|
||||
title: Modèle d'export
|
||||
|
|
@ -450,6 +450,11 @@ Rails.application.routes.draw do
|
|||
resources :procedures, only: [:index, :show], param: :procedure_id do
|
||||
member do
|
||||
resources :archives, only: [:index, :create]
|
||||
resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do
|
||||
collection do
|
||||
get 'preview'
|
||||
end
|
||||
end
|
||||
|
||||
resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do
|
||||
resource :contact_information
|
||||
|
|
12
db/migrate/20240130154452_create_export_templates.rb
Normal file
12
db/migrate/20240130154452_create_export_templates.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class CreateExportTemplates < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :export_templates do |t|
|
||||
t.string :name, null: false
|
||||
t.string :kind, null: false
|
||||
t.jsonb :content, default: {}
|
||||
t.belongs_to :groupe_instructeur, null: false, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
6
db/migrate/20240131094915_add_template_to_exports.rb
Normal file
6
db/migrate/20240131094915_add_template_to_exports.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class AddTemplateToExports < ActiveRecord::Migration[7.0]
|
||||
disable_ddl_transaction!
|
||||
def change
|
||||
add_reference :exports, :export_template, null: true, index: { algorithm: :concurrently }
|
||||
end
|
||||
end
|
5
db/migrate/20240131095645_add_export_template_fk.rb
Normal file
5
db/migrate/20240131095645_add_export_template_fk.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AddExportTemplateFk < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_foreign_key :exports, :export_templates, validate: false
|
||||
end
|
||||
end
|
5
db/migrate/20240131100329_validate_export_template_fk.rb
Normal file
5
db/migrate/20240131100329_validate_export_template_fk.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class ValidateExportTemplateFk < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
validate_foreign_key :exports, :export_templates
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -593,9 +593,20 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do
|
|||
t.index ["procedure_id"], name: "index_experts_procedures_on_procedure_id"
|
||||
end
|
||||
|
||||
create_table "export_templates", force: :cascade do |t|
|
||||
t.jsonb "content", default: {}
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "groupe_instructeur_id", null: false
|
||||
t.string "kind", null: false
|
||||
t.string "name", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["groupe_instructeur_id"], name: "index_export_templates_on_groupe_instructeur_id"
|
||||
end
|
||||
|
||||
create_table "exports", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.integer "dossiers_count"
|
||||
t.bigint "export_template_id"
|
||||
t.string "format", null: false
|
||||
t.bigint "instructeur_id"
|
||||
t.string "job_status", default: "pending", null: false
|
||||
|
@ -607,6 +618,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do
|
|||
t.datetime "updated_at", precision: nil, null: false
|
||||
t.bigint "user_profile_id"
|
||||
t.string "user_profile_type"
|
||||
t.index ["export_template_id"], name: "index_exports_on_export_template_id"
|
||||
t.index ["instructeur_id"], name: "index_exports_on_instructeur_id"
|
||||
t.index ["key"], name: "index_exports_on_key"
|
||||
t.index ["procedure_presentation_id"], name: "index_exports_on_procedure_presentation_id"
|
||||
|
@ -1224,6 +1236,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do
|
|||
add_foreign_key "experts", "users"
|
||||
add_foreign_key "experts_procedures", "experts"
|
||||
add_foreign_key "experts_procedures", "procedures"
|
||||
add_foreign_key "export_templates", "groupe_instructeurs"
|
||||
add_foreign_key "exports", "export_templates"
|
||||
add_foreign_key "exports", "instructeurs"
|
||||
add_foreign_key "france_connect_informations", "users"
|
||||
add_foreign_key "geo_areas", "champs"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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' }
|
||||
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
describe Instructeurs::ExportTemplatesController, type: :controller do
|
||||
before { sign_in(instructeur.user) }
|
||||
let(:tiptap_pdf_name) {
|
||||
{
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }
|
||||
]
|
||||
}.to_json
|
||||
}
|
||||
|
||||
let(:export_template_params) do
|
||||
{
|
||||
name: "coucou",
|
||||
kind: "zip",
|
||||
groupe_instructeur_id: groupe_instructeur.id,
|
||||
tiptap_pdf_name: tiptap_pdf_name,
|
||||
tiptap_default_dossier_directory: {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }
|
||||
]
|
||||
}.to_json,
|
||||
"pjs" =>
|
||||
[
|
||||
{ path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " _justif", "type" => "text" }] }] }, stable_id: "3" },
|
||||
{
|
||||
path:
|
||||
{ "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "cni_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] },
|
||||
stable_id: "5"
|
||||
},
|
||||
{
|
||||
path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "pj_repet_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] },
|
||||
stable_id: "10"
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:procedure) { create(:procedure, instructeurs: [instructeur]) }
|
||||
let(:groupe_instructeur) { procedure.defaut_groupe_instructeur }
|
||||
|
||||
describe '#create' do
|
||||
let(:subject) { post :create, params: { procedure_id: procedure.id, export_template: export_template_params } }
|
||||
|
||||
context 'with valid params' do
|
||||
it 'redirect to some page' do
|
||||
subject
|
||||
expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:))
|
||||
expect(flash.notice).to eq "Le modèle d'export coucou a bien été créé"
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
let(:tiptap_pdf_name) { { content: "invalid" }.to_json }
|
||||
it 'display error notification' do
|
||||
subject
|
||||
expect(flash.alert).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'with procedure not accessible by current instructeur' do
|
||||
let(:another_procedure) { create(:procedure) }
|
||||
let(:subject) { post :create, params: { procedure_id: another_procedure.id, export_template: export_template_params } }
|
||||
it 'raise exception' do
|
||||
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#edit' do
|
||||
let(:export_template) { create(:export_template, groupe_instructeur:) }
|
||||
let(:subject) { get :edit, params: { procedure_id: procedure.id, id: export_template.id } }
|
||||
|
||||
it 'render edit' do
|
||||
subject
|
||||
expect(response).to render_template(:edit)
|
||||
end
|
||||
|
||||
context "with export_template not accessible by current instructeur" do
|
||||
let(:another_groupe_instructeur) { create(:groupe_instructeur) }
|
||||
let(:export_template) { create(:export_template, groupe_instructeur: another_groupe_instructeur) }
|
||||
|
||||
it 'raise exception' do
|
||||
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update' do
|
||||
let(:export_template) { create(:export_template, groupe_instructeur:) }
|
||||
let(:tiptap_pdf_name) {
|
||||
{
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "exPort_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }
|
||||
]
|
||||
}.to_json
|
||||
}
|
||||
|
||||
let(:subject) { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } }
|
||||
|
||||
context 'with valid params' do
|
||||
it 'redirect to some page' do
|
||||
subject
|
||||
expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:))
|
||||
expect(flash.notice).to eq "Le modèle d'export coucou a bien été modifié"
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
let(:tiptap_pdf_name) { { content: "invalid" }.to_json }
|
||||
it 'display error notification' do
|
||||
subject
|
||||
expect(flash.alert).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#destroy' do
|
||||
let(:export_template) { create(:export_template, groupe_instructeur:) }
|
||||
let(:subject) { delete :destroy, params: { procedure_id: procedure.id, id: export_template.id } }
|
||||
|
||||
context 'with valid params' do
|
||||
it 'redirect to some page' do
|
||||
subject
|
||||
expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:))
|
||||
expect(flash.notice).to eq "Le modèle d'export Mon export a bien été supprimé"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
34
spec/factories/export_template.rb
Normal file
34
spec/factories/export_template.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
FactoryBot.define do
|
||||
factory :export_template do
|
||||
name { "Mon export" }
|
||||
groupe_instructeur
|
||||
content {
|
||||
{
|
||||
"pdf_name" =>
|
||||
{
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_id", "label" => "id dossier" } }, { "text" => " .pdf", "type" => "text" }] }
|
||||
]
|
||||
},
|
||||
"default_dossier_directory" =>
|
||||
{
|
||||
"type" => "doc",
|
||||
"content" =>
|
||||
[
|
||||
{
|
||||
"type" => "paragraph",
|
||||
"content" =>
|
||||
[
|
||||
{ "text" => "dossier_", "type" => "text" },
|
||||
{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
|
||||
{ "text" => " ", "type" => "text" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
kind { "zip" }
|
||||
end
|
||||
end
|
50
spec/models/concerns/pieces_jointes_list_concern_spec.rb
Normal file
50
spec/models/concerns/pieces_jointes_list_concern_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
describe PiecesJointesListConcern do
|
||||
describe '#pieces_jointes_list' do
|
||||
include Logic
|
||||
let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) }
|
||||
let(:types_de_champ_public) do
|
||||
[
|
||||
{ type: :integer_number, stable_id: 900 },
|
||||
{ type: :piece_justificative, libelle: "pj1", stable_id: 910 },
|
||||
{ type: :piece_justificative, libelle: "pj-cond", stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) },
|
||||
{ type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "pj2", stable_id: 921 }] },
|
||||
{ type: :titre_identite, libelle: "pj3", stable_id: 930 }
|
||||
]
|
||||
end
|
||||
|
||||
let(:types_de_champ_private) do
|
||||
[
|
||||
{ type: :integer_number, stable_id: 950 },
|
||||
{ type: :piece_justificative, libelle: "pj5", stable_id: 960 },
|
||||
{ type: :piece_justificative, libelle: "pj-cond2", stable_id: 961, condition: ds_eq(champ_value(900), constant(1)) },
|
||||
{ type: :repetition, libelle: "Répétition2", stable_id: 970, children: [{ type: :piece_justificative, libelle: "pj6", stable_id: 971 }] }
|
||||
]
|
||||
end
|
||||
|
||||
let(:types_de_champ) { procedure.active_revision.types_de_champ }
|
||||
def find_by_stable_id(stable_id) = types_de_champ.find { _1.stable_id == stable_id }
|
||||
|
||||
let(:pj1) { find_by_stable_id(910) }
|
||||
let(:pjcond) { find_by_stable_id(911) }
|
||||
let(:repetition) { find_by_stable_id(920) }
|
||||
let(:pj2) { find_by_stable_id(921) }
|
||||
let(:pj3) { find_by_stable_id(930) }
|
||||
|
||||
let(:pj5) { find_by_stable_id(960) }
|
||||
let(:pjcond2) { find_by_stable_id(961) }
|
||||
let(:repetition2) { find_by_stable_id(970) }
|
||||
let(:pj6) { find_by_stable_id(971) }
|
||||
|
||||
it "returns the list of pieces jointes without conditional" do
|
||||
expect(procedure.public_wrapped_partionned_pjs.first).to match_array([[pj1], [pj2, repetition], [pj3]])
|
||||
end
|
||||
|
||||
it "returns the list of pieces jointes having conditional" do
|
||||
expect(procedure.public_wrapped_partionned_pjs.second).to match_array([[pjcond]])
|
||||
end
|
||||
|
||||
it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do
|
||||
expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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)
|
||||
|
|
317
spec/models/export_template_spec.rb
Normal file
317
spec/models/export_template_spec.rb
Normal file
|
@ -0,0 +1,317 @@
|
|||
describe ExportTemplate do
|
||||
let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) }
|
||||
let(:export_template) { create(:export_template, groupe_instructeur:, content:) }
|
||||
let(:procedure) { create(:procedure_with_dossiers, types_de_champ_public:, for_individual:) }
|
||||
let(:dossier) { procedure.dossiers.first }
|
||||
let(:for_individual) { false }
|
||||
let(:types_de_champ_public) do
|
||||
[
|
||||
{ type: :piece_justificative, libelle: "Justificatif de domicile", mandatory: true, stable_id: 3 },
|
||||
{ type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 5 }
|
||||
]
|
||||
end
|
||||
let(:content) do
|
||||
{
|
||||
"pdf_name" => {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }
|
||||
]
|
||||
},
|
||||
"default_dossier_directory" => {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }
|
||||
]
|
||||
},
|
||||
"pjs" =>
|
||||
[
|
||||
{ path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "original-filename", "label" => "nom original du fichier" } }, { "text" => " _justif", "type" => "text" }] }] }, stable_id: "3" },
|
||||
{
|
||||
path:
|
||||
{ "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "cni_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] },
|
||||
stable_id: "5"
|
||||
},
|
||||
{
|
||||
path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "pj_repet_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] },
|
||||
stable_id: "10"
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
describe 'new' do
|
||||
let(:export_template) { build(:export_template, groupe_instructeur: groupe_instructeur) }
|
||||
it 'set default values' do
|
||||
export_template.set_default_values
|
||||
expect(export_template.content).to eq({
|
||||
"pdf_name" => {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }
|
||||
]
|
||||
},
|
||||
"default_dossier_directory" => {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "dossier-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }
|
||||
]
|
||||
},
|
||||
"pjs" =>
|
||||
[
|
||||
|
||||
{
|
||||
"stable_id" => "3",
|
||||
"path" => { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "justificatif-de-domicile-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }] }
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tiptap_default_dossier_directory' do
|
||||
it 'returns tiptap_default_dossier_directory from content' do
|
||||
expect(export_template.tiptap_default_dossier_directory).to eq({
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }
|
||||
]
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tiptap_pdf_name' do
|
||||
it 'returns tiptap_pdf_name from content' do
|
||||
expect(export_template.tiptap_pdf_name).to eq({
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }
|
||||
]
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content_for_pj' do
|
||||
let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) }
|
||||
let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) }
|
||||
|
||||
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
|
||||
|
||||
it 'returns tiptap content for pj' do
|
||||
expect(export_template.content_for_pj(type_de_champ_pj)).to eq({
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "original-filename", "label" => "nom original du fichier" } }, { "text" => " _justif", "type" => "text" }] }
|
||||
]
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#attachment_and_path' do
|
||||
let(:dossier) { create(:dossier) }
|
||||
|
||||
context 'for export pdf' do
|
||||
let(:attachment) { double("attachment") }
|
||||
|
||||
it 'gives absolute filename for export of specific dossier' do
|
||||
allow(attachment).to receive(:name).and_return('pdf_export_for_instructeur')
|
||||
expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/mon_export_#{dossier.id}.pdf"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'for pj' do
|
||||
let(:dossier) { procedure.dossiers.first }
|
||||
let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, procedure:) }
|
||||
let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) }
|
||||
|
||||
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
|
||||
|
||||
before do
|
||||
dossier.champs_public << champ_pj
|
||||
end
|
||||
it 'returns pj and custom name for pj' do
|
||||
expect(export_template.attachment_and_path(dossier, attachment, champ: champ_pj)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif-1.png"])
|
||||
end
|
||||
end
|
||||
context 'pj repetable' do
|
||||
let(:procedure) do
|
||||
create(:procedure_with_dossiers, :for_individual, types_de_champ_public: [{ type: :repetition, mandatory: true, children: [{ libelle: 'sub type de champ' }] }])
|
||||
end
|
||||
let(:type_de_champ_repetition) do
|
||||
repetition = draft.types_de_champ_public.repetition.first
|
||||
repetition.update(stable_id: 3333)
|
||||
repetition
|
||||
end
|
||||
let(:draft) { procedure.draft_revision }
|
||||
let(:dossier) { procedure.dossiers.first }
|
||||
|
||||
let(:type_de_champ_pj) do
|
||||
draft.add_type_de_champ({
|
||||
type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative),
|
||||
libelle: "pj repet",
|
||||
stable_id: 10,
|
||||
parent_stable_id: type_de_champ_repetition.stable_id
|
||||
})
|
||||
end
|
||||
let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) }
|
||||
|
||||
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
|
||||
|
||||
before do
|
||||
dossier.champs_public << champ_pj
|
||||
end
|
||||
it 'rename repetable pj' do
|
||||
expect(export_template.attachment_and_path(dossier, attachment, champ: champ_pj)).to eq([attachment, "DOSSIER_#{dossier.id}/pj_repet_#{dossier.id}-1.png"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tiptap_convert' do
|
||||
it 'convert default dossier directory' do
|
||||
expect(export_template.tiptap_convert(procedure.dossiers.first, "default_dossier_directory")).to eq "DOSSIER_#{dossier.id}"
|
||||
end
|
||||
|
||||
it 'convert pdf_name' do
|
||||
expect(export_template.tiptap_convert(procedure.dossiers.first, "pdf_name")).to eq "mon_export_#{dossier.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tiptap_convert_pj' do
|
||||
let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) }
|
||||
let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) }
|
||||
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
|
||||
|
||||
it 'convert pj' do
|
||||
attachment
|
||||
expect(export_template.tiptap_convert_pj(dossier, type_de_champ_pj.stable_id, attachment)).to eq "superpj_justif"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
let(:subject) { build(:export_template, groupe_instructeur:, content:) }
|
||||
let(:ddd_text) { "DoSSIER" }
|
||||
let(:mention) { { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } } }
|
||||
let(:ddd_mention) { mention }
|
||||
let(:pdf_text) { "export" }
|
||||
let(:pdf_mention) { mention }
|
||||
let(:pj_text) { "_pj" }
|
||||
let(:pj_mention) { mention }
|
||||
let(:content) do
|
||||
{
|
||||
"pdf_name" => {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => pdf_text, "type" => "text" }, pdf_mention] }
|
||||
]
|
||||
},
|
||||
"default_dossier_directory" => {
|
||||
"type" => "doc",
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => ddd_text, "type" => "text" }, ddd_mention] }
|
||||
]
|
||||
},
|
||||
"pjs" =>
|
||||
[
|
||||
{ path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [pj_mention, { "text" => pj_text, "type" => "text" }] }] }, stable_id: "3" }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
context 'with valid default dossier directory' do
|
||||
it 'has no error for default_dossier_directory' do
|
||||
expect(subject.valid?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no ddd text' do
|
||||
let(:ddd_text) { " " }
|
||||
context 'with mention' do
|
||||
let(:ddd_mention) { { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } } }
|
||||
it 'has no error for default_dossier_directory' do
|
||||
expect(subject.valid?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'without numéro de dossier' do
|
||||
let(:ddd_mention) { { "type" => "mention", "attrs" => { "id" => 'dossier_service_name', "label" => "nom du service" } } }
|
||||
it "add error for tiptap_default_dossier_directory" do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject.errors[:tiptap_default_dossier_directory]).to be_present
|
||||
expect(subject.errors.full_messages).to include "Le champ « Nom du répertoire » doit contenir le numéro du dossier"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid pdf name' do
|
||||
it 'has no error for pdf name' do
|
||||
expect(subject.valid?).to be_truthy
|
||||
expect(subject.errors[:tiptap_pdf_name]).not_to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pdf text and without mention' do
|
||||
let(:pdf_text) { "export" }
|
||||
let(:pdf_mention) { { "type" => "mention", "attrs" => {} } }
|
||||
|
||||
it "add no error" do
|
||||
expect(subject.valid?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no pdf text' do
|
||||
let(:pdf_text) { " " }
|
||||
|
||||
context 'with mention' do
|
||||
it 'has no error for default_dossier_directory' do
|
||||
expect(subject.valid?).to be_truthy
|
||||
expect(subject.errors[:tiptap_pdf_name]).not_to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'without mention' do
|
||||
let(:pdf_mention) { { "type" => "mention", "attrs" => {} } }
|
||||
it "add error for pdf name" do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject.errors.full_messages).to include "Le champ « Nom du dossier au format pdf » doit être rempli"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no pj text' do
|
||||
# let!(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) }
|
||||
let(:pj_text) { " " }
|
||||
|
||||
context 'with mention' do
|
||||
it 'has no error for pj' do
|
||||
expect(subject.valid?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'without mention' do
|
||||
let(:pj_mention) { { "type" => "mention", "attrs" => {} } }
|
||||
it "add error for pj" do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject.errors.full_messages).to include "Le champ « Justificatif de domicile » doit être rempli"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'specific_tags' do
|
||||
context 'for entreprise procedure' do
|
||||
let(:for_individual) { false }
|
||||
it do
|
||||
tags = export_template.specific_tags
|
||||
expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"]
|
||||
end
|
||||
end
|
||||
|
||||
context 'for individual procedure' do
|
||||
let(:for_individual) { true }
|
||||
it do
|
||||
tags = export_template.specific_tags
|
||||
expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1746,32 +1746,6 @@ describe Procedure do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#pieces_jointes_list' do
|
||||
include Logic
|
||||
let(:procedure) { create(:procedure, types_de_champ_public:) }
|
||||
let(:types_de_champ_public) do
|
||||
[
|
||||
{ type: :integer_number, stable_id: 900 },
|
||||
{ type: :piece_justificative, libelle: "PJ", mandatory: true, stable_id: 910 },
|
||||
{ type: :piece_justificative, libelle: "PJ-cond", mandatory: true, stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) },
|
||||
{ type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "PJ2", stable_id: 921 }] }
|
||||
]
|
||||
end
|
||||
|
||||
let(:pj1) { procedure.active_revision.types_de_champ.find { _1.stable_id == 910 } }
|
||||
let(:pjcond) { procedure.active_revision.types_de_champ.find { _1.stable_id == 911 } }
|
||||
let(:repetition) { procedure.active_revision.types_de_champ.find { _1.stable_id == 920 } }
|
||||
let(:pj2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 921 } }
|
||||
|
||||
it "returns the list of pieces jointes without conditional" do
|
||||
expect(procedure.pieces_jointes_list_without_conditionnal).to match_array([[pj1], [pj2, repetition]])
|
||||
end
|
||||
|
||||
it "returns the list of pieces jointes having conditional" do
|
||||
expect(procedure.pieces_jointes_list_with_conditionnal).to match_array([[pjcond]])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#attestation_template" do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
|
|
|
@ -1,9 +1,98 @@
|
|||
describe PiecesJustificativesService do
|
||||
describe 'pjs_for_champs' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative }, { type: :repetition, children: [{ type: :piece_justificative }] }]) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:dossiers) { Dossier.where(id: dossier.id) }
|
||||
let(:witness) { create(:dossier, procedure: procedure) }
|
||||
let(:export_template) { double('ExportTemplate') }
|
||||
let(:pj_service) { PiecesJustificativesService.new(user_profile:, export_template:) }
|
||||
let(:user_profile) { build(:administrateur) }
|
||||
|
||||
def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp')
|
||||
def repetition(d) = d.champs.find_by(type: "Champs::RepetitionChamp")
|
||||
def attachments(champ) = champ.piece_justificative_file.attachments
|
||||
|
||||
before { attach_file_to_champ(pj_champ(witness)) }
|
||||
|
||||
subject { pj_service.send(:pjs_for_champs, dossiers) }
|
||||
|
||||
context 'without any attachment' do
|
||||
it { expect(subject).to be_empty }
|
||||
end
|
||||
|
||||
context 'with a single attachment' do
|
||||
let(:champ) { pj_champ(dossier) }
|
||||
before { attach_file_to_champ(champ) }
|
||||
|
||||
it do
|
||||
expect(export_template).to receive(:attachment_and_path)
|
||||
.with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple attachments' do
|
||||
let(:champ) { pj_champ(dossier) }
|
||||
|
||||
before do
|
||||
attach_file_to_champ(champ)
|
||||
attach_file_to_champ(champ)
|
||||
end
|
||||
|
||||
it do
|
||||
expect(export_template).to receive(:attachment_and_path)
|
||||
.with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:)
|
||||
|
||||
expect(export_template).to receive(:attachment_and_path)
|
||||
.with(dossier, attachments(pj_champ(dossier)).second, index: 1, row_index: nil, champ:)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a repetition' do
|
||||
let(:first_champ) { repetition(dossier).champs.first }
|
||||
let(:second_champ) { repetition(dossier).champs.second }
|
||||
|
||||
before do
|
||||
repetition(dossier).add_row(dossier.revision)
|
||||
attach_file_to_champ(first_champ)
|
||||
attach_file_to_champ(first_champ)
|
||||
|
||||
repetition(dossier).add_row(dossier.revision)
|
||||
attach_file_to_champ(second_champ)
|
||||
end
|
||||
|
||||
it do
|
||||
first_child_attachments = attachments(repetition(dossier).champs.first)
|
||||
second_child_attachments = attachments(repetition(dossier).champs.second)
|
||||
|
||||
expect(export_template).to receive(:attachment_and_path)
|
||||
.with(dossier, first_child_attachments.first, index: 0, row_index: 0, champ: first_champ)
|
||||
|
||||
expect(export_template).to receive(:attachment_and_path)
|
||||
.with(dossier, first_child_attachments.second, index: 1, row_index: 0, champ: first_champ)
|
||||
|
||||
expect(export_template).to receive(:attachment_and_path)
|
||||
.with(dossier, second_child_attachments.first, index: 0, row_index: 1, champ: second_champ)
|
||||
|
||||
count = 0
|
||||
|
||||
callback = lambda { |*_args| count += 1 }
|
||||
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
|
||||
subject
|
||||
end
|
||||
|
||||
expect(count).to eq(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 +108,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 +397,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 +409,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
|
||||
|
@ -323,6 +417,67 @@ describe PiecesJustificativesService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#compute_champ_id_row_index' do
|
||||
let(:user_profile) { build(:administrateur) }
|
||||
let(:types_de_champ_public) do
|
||||
[
|
||||
{ type: :repetition, children: [{ type: :piece_justificative }] },
|
||||
{ type: :repetition, children: [{ type: :piece_justificative }, { type: :piece_justificative }] }
|
||||
]
|
||||
end
|
||||
|
||||
let(:procedure) { create(:procedure, types_de_champ_public:) }
|
||||
let(:dossier_1) { create(:dossier, procedure:) }
|
||||
let(:champs) { dossier_1.champs }
|
||||
|
||||
def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp')
|
||||
def repetition(d, index:) = d.champs_public.filter(&:repetition?)[index]
|
||||
|
||||
subject { PiecesJustificativesService.new(user_profile:, export_template: nil).send(:compute_champ_id_row_index, champs) }
|
||||
|
||||
before do
|
||||
pj_champ(dossier_1)
|
||||
|
||||
# repet_0 (stable_id: r0)
|
||||
# # row_0
|
||||
# # # pj_champ_0 (stable_id: 0)
|
||||
# # row_1
|
||||
# # # pj_champ_1 (stable_id: 0)
|
||||
# repet_1 (stable_id: r1)
|
||||
# # row_0
|
||||
# # # pj_champ_2 (stable_id: 1)
|
||||
# # # pj_champ_3 (stable_id: 2)
|
||||
# # row_1
|
||||
# # # pj_champ_4 (stable_id: 1)
|
||||
# # # pj_champ_5 (stable_id: 2)
|
||||
|
||||
repet_0 = repetition(dossier_1, index: 0)
|
||||
repet_1 = repetition(dossier_1, index: 1)
|
||||
|
||||
repet_0.add_row(dossier_1.revision)
|
||||
repet_0.add_row(dossier_1.revision)
|
||||
|
||||
repet_1.add_row(dossier_1.revision)
|
||||
repet_1.add_row(dossier_1.revision)
|
||||
end
|
||||
|
||||
it do
|
||||
champs = dossier_1.champs_public
|
||||
repet_0 = champs[0]
|
||||
pj_0 = repet_0.rows.first.first
|
||||
pj_1 = repet_0.rows.second.first
|
||||
|
||||
repet_1 = champs[1]
|
||||
pj_2 = repet_1.rows.first.first
|
||||
pj_3 = repet_1.rows.first.second
|
||||
|
||||
pj_4 = repet_1.rows.second.first
|
||||
pj_5 = repet_1.rows.second.second
|
||||
|
||||
is_expected.to eq({ pj_0.id => 0, pj_1.id => 1, pj_2.id => 0, pj_3.id => 0, pj_4.id => 1, pj_5.id => 1 })
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file_to_champ(champ, safe = true)
|
||||
attach_file(champ.piece_justificative_file, safe)
|
||||
end
|
||||
|
|
|
@ -33,11 +33,11 @@ describe ProcedureArchiveService do
|
|||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
|
||||
structure = [
|
||||
"#{service.send(:zip_root_folder, archive)}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2021-00-00-#{dossier.attestation.pdf.id % 10000}.pdf",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf"
|
||||
"export/",
|
||||
"export/dossier-#{dossier.id}/",
|
||||
"export/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"export/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2021-00-00-#{dossier.attestation.pdf.id % 10000}.pdf",
|
||||
"export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf"
|
||||
]
|
||||
expect(files.map(&:filename)).to match_array(structure)
|
||||
end
|
||||
|
@ -53,11 +53,11 @@ describe ProcedureArchiveService do
|
|||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
structure = [
|
||||
"#{service.send(:zip_root_folder, archive)}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/-LISTE-DES-FICHIERS-EN-ERREURS.txt",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf"
|
||||
"export/",
|
||||
"export/-LISTE-DES-FICHIERS-EN-ERREURS.txt",
|
||||
"export/dossier-#{dossier.id}/",
|
||||
"export/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf"
|
||||
]
|
||||
expect(files.map(&:filename)).to match_array(structure)
|
||||
end
|
||||
|
@ -100,12 +100,12 @@ describe ProcedureArchiveService do
|
|||
archive.file.open do |f|
|
||||
zip_entries = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
structure = [
|
||||
"#{service.send(:zip_root_folder, archive)}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/-LISTE-DES-FICHIERS-EN-ERREURS.txt",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-dossier-05-03-2020-00-00-1.pdf",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf"
|
||||
"export/",
|
||||
"export/-LISTE-DES-FICHIERS-EN-ERREURS.txt",
|
||||
"export/dossier-#{dossier.id}/",
|
||||
"export/dossier-#{dossier.id}/export-dossier-05-03-2020-00-00-1.pdf",
|
||||
"export/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf"
|
||||
]
|
||||
expect(zip_entries.map(&:filename)).to match_array(structure)
|
||||
zip_entries.map do |entry|
|
||||
|
@ -134,15 +134,15 @@ describe ProcedureArchiveService do
|
|||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
structure = [
|
||||
"#{service.send(:zip_root_folder, archive)}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier.attestation.pdf.id % 10000}.pdf",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2020-00-00-#{dossier.id % 10000}.pdf",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/export-#{dossier_2020.id}-05-03-2020-00-00-#{dossier_2020.id % 10000}.pdf",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/pieces_justificatives/",
|
||||
"#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier_2020.attestation.pdf.id % 10000}.pdf"
|
||||
"export/",
|
||||
"export/dossier-#{dossier.id}/",
|
||||
"export/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"export/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier.attestation.pdf.id % 10000}.pdf",
|
||||
"export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2020-00-00-#{dossier.id % 10000}.pdf",
|
||||
"export/dossier-#{dossier_2020.id}/",
|
||||
"export/dossier-#{dossier_2020.id}/export-#{dossier_2020.id}-05-03-2020-00-00-#{dossier_2020.id % 10000}.pdf",
|
||||
"export/dossier-#{dossier_2020.id}/pieces_justificatives/",
|
||||
"export/dossier-#{dossier_2020.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier_2020.attestation.pdf.id % 10000}.pdf"
|
||||
]
|
||||
expect(files.map(&:filename)).to match_array(structure)
|
||||
end
|
||||
|
|
|
@ -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,38 +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}-1.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)
|
||||
structure = [
|
||||
"#{service.send(:base_filename)}/",
|
||||
"#{service.send(:base_filename)}/dossier-#{dossier.id}/",
|
||||
"#{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(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
|
||||
|
|
86
spec/services/procedure_export_service_zip_spec.rb
Normal file
86
spec/services/procedure_export_service_zip_spec.rb
Normal file
|
@ -0,0 +1,86 @@
|
|||
describe ProcedureExportService do
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative, libelle: 'repet_pj' }] }]) }
|
||||
let(:dossiers) { create_list(:dossier, 10, procedure: procedure) }
|
||||
let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) }
|
||||
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
|
||||
|
||||
def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp')
|
||||
def repetition(d) = d.champs.find_by(type: "Champs::RepetitionChamp")
|
||||
def attachments(champ) = champ.piece_justificative_file.attachments
|
||||
|
||||
before do
|
||||
dossiers.each do |dossier|
|
||||
attach_file_to_champ(pj_champ(dossier))
|
||||
|
||||
repetition(dossier).add_row(dossier.revision)
|
||||
attach_file_to_champ(repetition(dossier).champs.first)
|
||||
attach_file_to_champ(repetition(dossier).champs.first)
|
||||
|
||||
repetition(dossier).add_row(dossier.revision)
|
||||
attach_file_to_champ(repetition(dossier).champs.second)
|
||||
end
|
||||
|
||||
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
|
||||
end
|
||||
|
||||
describe 'to_zip' do
|
||||
subject { service.to_zip }
|
||||
|
||||
describe 'generate_dossiers_export' do
|
||||
context 'with export_template' do
|
||||
let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) }
|
||||
|
||||
it 'returns a blob with custom filenames' do
|
||||
VCR.use_cassette('archive/new_file_to_get_200', allow_playback_repeats: true) do
|
||||
sql_count = 0
|
||||
|
||||
callback = lambda { |*_args| sql_count += 1 }
|
||||
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
|
||||
subject
|
||||
end
|
||||
|
||||
expect(sql_count <= 58).to be_truthy
|
||||
|
||||
dossier = dossiers.first
|
||||
|
||||
File.write('tmp.zip', subject.download, mode: 'wb')
|
||||
File.open('tmp.zip') do |fd|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: fd)
|
||||
structure = [
|
||||
"export/",
|
||||
"export/dossier-#{dossier.id}/",
|
||||
"export/dossier-#{dossier.id}/export_#{dossier.id}.pdf",
|
||||
"export/dossier-#{dossier.id}/pj-#{dossier.id}-1.png",
|
||||
"export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-1-1.png",
|
||||
"export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-2-1.png",
|
||||
"export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-1-2.png"
|
||||
]
|
||||
|
||||
expect(files.size).to eq(dossiers.count * 6 + 1)
|
||||
expect(structure - files.map(&:filename)).to be_empty
|
||||
end
|
||||
FileUtils.remove_entry_secure('tmp.zip')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file_to_champ(champ, safe = true)
|
||||
attach_file(champ.piece_justificative_file, safe)
|
||||
end
|
||||
|
||||
def attach_file(attachable, safe = true)
|
||||
to_be_attached = {
|
||||
io: StringIO.new("toto"),
|
||||
filename: "toto.png", content_type: "image/png"
|
||||
}
|
||||
|
||||
if safe
|
||||
to_be_attached[:metadata] = { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
end
|
||||
|
||||
attachable.attach(to_be_attached)
|
||||
end
|
||||
end
|
|
@ -192,4 +192,20 @@ RSpec.describe TiptapService do
|
|||
expect(described_class.new.used_tags_and_libelle_for(json)).to eq(Set.new([['name', 'Nom']]))
|
||||
end
|
||||
end
|
||||
|
||||
describe '.to_path' do
|
||||
let(:substitutions) { { "dossier_number" => "42" } }
|
||||
let(:json) do
|
||||
{
|
||||
"content" => [
|
||||
{ "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " .pdf", "type" => "text" }] }
|
||||
]
|
||||
|
||||
}.deep_symbolize_keys
|
||||
end
|
||||
|
||||
it 'returns path' do
|
||||
expect(described_class.new.to_path(json, substitutions)).to eq("export_42.pdf")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,7 +110,7 @@ describe 'shared/_procedure_description', type: :view do
|
|||
|
||||
context 'caching', caching: true do
|
||||
it "works" do
|
||||
expect(procedure).to receive(:pieces_jointes_list?).once
|
||||
expect(procedure).to receive(:public_wrapped_partionned_pjs).once
|
||||
2.times { render partial: 'shared/procedure_description', locals: { procedure: } }
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue