Merge pull request #10548 from demarches-simplifiees/choose_pjs_to_export

ETQ Instructeur, je peux choisir les pjs que j'exporte
This commit is contained in:
LeSim 2024-07-26 11:43:02 +00:00 committed by GitHub
commit 12c174f6cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1014 additions and 889 deletions

View file

@ -51,6 +51,7 @@
&.no-list {
ul {
list-style: none !important;
padding-left: 0;
}
}

View file

@ -175,6 +175,30 @@
}
}
&-file-image-line {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z'%3E%3C/path%3E%3C/svg%3E");
}
}
&-folder-line {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z'%3E%3C/path%3E%3C/svg%3E");
}
}
&-folder-zip-line {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10.4142 3L12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142ZM18 18H14V15H16V13H14V11H16V9H14V7H11.5858L9.58579 5H4V19H20V7H16V9H18V11H16V13H18V18Z'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10.4142 3L12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142ZM18 18H14V15H16V13H14V11H16V9H14V7H11.5858L9.58579 5H4V19H20V7H16V9H18V11H16V13H18V18Z'%3E%3C/path%3E%3C/svg%3E");
}
}
&-intermediate-circle-fill {
&:before,
&:after {
@ -191,6 +215,14 @@
}
}
&-pdf-2-line {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M5 4H15V8H19V20H5V4ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM10.4999 7.5C10.4999 9.07749 10.0442 10.9373 9.27493 12.6534C8.50287 14.3757 7.46143 15.8502 6.37524 16.7191L7.55464 18.3321C10.4821 16.3804 13.7233 15.0421 16.8585 15.49L17.3162 13.5513C14.6435 12.6604 12.4999 9.98994 12.4999 7.5H10.4999ZM11.0999 13.4716C11.3673 12.8752 11.6042 12.2563 11.8037 11.6285C12.2753 12.3531 12.8553 13.0182 13.5101 13.5953C12.5283 13.7711 11.5665 14.0596 10.6352 14.4276C10.7999 14.1143 10.9551 13.7948 11.0999 13.4716Z'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M5 4H15V8H19V20H5V4ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM10.4999 7.5C10.4999 9.07749 10.0442 10.9373 9.27493 12.6534C8.50287 14.3757 7.46143 15.8502 6.37524 16.7191L7.55464 18.3321C10.4821 16.3804 13.7233 15.0421 16.8585 15.49L17.3162 13.5513C14.6435 12.6604 12.4999 9.98994 12.4999 7.5H10.4999ZM11.0999 13.4716C11.3673 12.8752 11.6042 12.2563 11.8037 11.6285C12.2753 12.3531 12.8553 13.0182 13.5101 13.5953C12.5283 13.7711 11.5665 14.0596 10.6352 14.4276C10.7999 14.1143 10.9551 13.7948 11.0999 13.4716Z'%3E%3C/path%3E%3C/svg%3E");
}
}
&-underline {
&:before,
&:after {

View file

@ -22,5 +22,5 @@
= 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}"
- menu.with_item do
= link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do
= link_to [:new, :instructeur, @procedure, :export_template], role: 'menuitem' do
Ajouter un modèle d'export

View file

@ -1,21 +1,18 @@
module Instructeurs
class ExportTemplatesController < InstructeurController
before_action :set_procedure
before_action :set_groupe_instructeur, only: [:create, :update]
before_action :set_procedure_and_groupe_instructeurs
before_action :set_export_template, only: [:edit, :update, :destroy]
before_action :set_groupe_instructeurs
before_action :set_all_pj
before_action :ensure_legitimate_groupe_instructeur, only: [:create, :update]
def new
@export_template = ExportTemplate.new(kind: 'zip', groupe_instructeur: @groupe_instructeurs.first)
@export_template.set_default_values
@export_template = ExportTemplate.default(groupe_instructeur: @groupe_instructeurs.first)
end
def create
@export_template = @groupe_instructeur.export_templates.build(export_template_params)
@export_template.assign_pj_names(pj_params)
@export_template = ExportTemplate.new(export_template_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éé"
redirect_to [:exports, :instructeur, @procedure], notice: "Le modèle d'export #{@export_template.name} a bien été créé"
else
flash[:alert] = @export_template.errors.full_messages
render :new
@ -26,11 +23,8 @@ module Instructeurs
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é"
if @export_template.update(export_template_params)
redirect_to [:exports, :instructeur, @procedure], notice: "Le modèle d'export #{@export_template.name} a bien été modifié"
else
flash[:alert] = @export_template.errors.full_messages
render :edit
@ -39,62 +33,40 @@ module Instructeurs
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é"
redirect_to [:exports, :instructeur, @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é"
redirect_to [:exports, :instructeur, @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)
export_template = ExportTemplate.new(export_template_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 })
render turbo_stream: turbo_stream.replace('preview', partial: 'preview', locals: { export_template: })
end
private
def export_template_params
params.require(:export_template).permit(*export_params)
params.require(:export_template)
.permit(:name, :kind, :groupe_instructeur_id, dossier_folder: [:enabled, :template], export_pdf: [:enabled, :template], pjs: [:stable_id, :enabled, :template])
end
def set_procedure
@procedure = current_instructeur.procedures.find params[:procedure_id]
Sentry.configure_scope do |scope|
scope.set_tags(procedure: @procedure.id)
end
def set_procedure_and_groupe_instructeurs
@procedure = current_instructeur.procedures.find(params[:procedure_id])
@groupe_instructeurs = current_instructeur.groupe_instructeurs.where(procedure: @procedure)
Sentry.configure_scope { |scope| scope.set_tags(procedure: @procedure.id) }
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 ensure_legitimate_groupe_instructeur
return if export_template_params[:groupe_instructeur_id].in?(@groupe_instructeurs.map { _1.id.to_s })
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)
redirect_to [:exports, :instructeur, @procedure], alert: 'Vous navez pas le droit de créer un modèle dexport pour ce groupe'
end
end
end

View file

@ -150,4 +150,6 @@ module ApplicationHelper
.map { |word| word[0].upcase }
.join
end
def asterisk = render(EditableChamp::AsteriskMandatoryComponent.new)
end

View file

@ -0,0 +1,19 @@
import { Controller } from '@hotwired/stimulus';
export class HideTargetController extends Controller {
static targets = ['source', 'toHide'];
declare readonly toHideTargets: HTMLDivElement[];
declare readonly sourceTargets: HTMLInputElement[];
connect() {
this.sourceTargets.forEach((source) => {
source.addEventListener('click', this.handleInput.bind(this));
});
}
handleInput() {
this.toHideTargets.forEach((toHide) => {
toHide.classList.toggle('fr-hidden');
});
}
}

View file

@ -10,7 +10,8 @@ import { createEditor } from '../../shared/tiptap/editor';
export class TiptapController extends ApplicationController {
static targets = ['editor', 'input', 'button', 'tag'];
static values = {
insertAfterTag: { type: String, default: '' }
insertAfterTag: { type: String, default: '' },
attributes: { type: Object, default: {} }
};
declare editorTarget: Element;
@ -18,6 +19,7 @@ export class TiptapController extends ApplicationController {
declare buttonTargets: HTMLButtonElement[];
declare tagTargets: HTMLElement[];
declare insertAfterTagValue: string;
declare attributesValue: Record<string, string>;
#initializing = true;
#editor?: Editor;
@ -28,6 +30,7 @@ export class TiptapController extends ApplicationController {
content: this.content,
tags: this.tags,
buttons: this.menuButtons,
attributes: { class: 'fr-input', ...this.attributesValue },
onChange: ({ editor }) => {
for (const button of this.buttonTargets) {
const action = getAction(editor, button);

View file

@ -0,0 +1,20 @@
import { Controller } from '@hotwired/stimulus';
export class TiptapToTemplateController extends Controller {
static targets = ['output', 'trigger'];
declare readonly outputTarget: HTMLElement;
declare readonly triggerTarget: HTMLButtonElement;
connect() {
this.triggerTarget.addEventListener('click', this.handleClick.bind(this));
}
handleClick() {
const template = this.element.querySelector('.tiptap.ProseMirror p');
if (template) {
this.outputTarget.innerHTML = template.innerHTML;
}
}
}

View file

@ -33,6 +33,7 @@ export function createEditor({
content,
tags,
buttons,
attributes,
onChange
}: {
editorElement: Element;
@ -40,8 +41,15 @@ export function createEditor({
tags: TagSchema[];
buttons: string[];
onChange(change: { editor: Editor }): void;
attributes?: Record<string, string>;
}): Editor {
const options = getEditorOptions(editorElement, tags, buttons, content);
const options = getEditorOptions(
editorElement,
tags,
buttons,
content,
attributes
);
const editor = new Editor(options);
editor.on('transaction', onChange);
return editor;
@ -51,7 +59,8 @@ function getEditorOptions(
element: Element,
tags: TagSchema[],
actions: string[],
content?: JSONContent
content?: JSONContent,
attributes?: Record<string, string>
): Partial<EditorOptions> {
const extensions: Extensions = [];
for (const action of actions) {
@ -123,7 +132,7 @@ function getEditorOptions(
return {
element,
content,
editorProps: { attributes: { class: 'fr-input' } },
editorProps: { attributes },
extensions: [
actions.includes('title') ? DocumentWithHeader : Document,
Hystory,

View file

@ -11,14 +11,26 @@ module PiecesJointesListConcern
pieces_jointes(exclude_titre_identite: true)
end
def exportables_pieces_jointes_for_all_versions
pieces_jointes(
exclude_titre_identite: true,
revision: revisions
).sort_by { - _1.id }.uniq(&:stable_id)
end
def outdated_exportables_pieces_jointes
exportables_pieces_jointes_for_all_versions - exportables_pieces_jointes
end
private
def pieces_jointes(
exclude_titre_identite: false,
public_only: false,
wrap_with_parent: false
wrap_with_parent: false,
revision: active_revision
)
coordinates = active_revision.revision_types_de_champ
coordinates = ProcedureRevisionTypeDeChamp.where(revision:)
.includes(:type_de_champ, revision_types_de_champ: :type_de_champ)
coordinates = coordinates.public_only if public_only

70
app/models/export_item.rb Normal file
View file

@ -0,0 +1,70 @@
class ExportItem
include TagsSubstitutionConcern
DOSSIER_STATE = Dossier.states.fetch(:en_construction)
FORMAT_DATE = "%Y-%m-%d".freeze
attr_reader :template, :enabled, :stable_id
def initialize(template:, enabled: true, stable_id: nil)
@template, @enabled, @stable_id = template, enabled, stable_id
end
def self.default(prefix:, enabled: true, stable_id: nil)
new(template: prefix_dossier_id(prefix), enabled:, stable_id:)
end
def self.default_pj(tdc)
default(prefix: tdc.libelle_as_filename, enabled: false, stable_id: tdc.stable_id)
end
def enabled? = enabled
def template_json = template.to_json
def template_string = TiptapService.new.to_texts_and_tags(template)
def path(dossier, attachment: nil, row_index: nil, index: nil)
used_tags = TiptapService.used_tags_and_libelle_for(template)
substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true)
substitutions['original-filename'] = attachment.filename.base if attachment
TiptapService.new.to_texts_and_tags(template, substitutions) + suffix(attachment, row_index, index)
end
def ==(other)
self.class == other.class &&
template == other.template &&
enabled == other.enabled &&
stable_id == other.stable_id
end
private
def self.prefix_dossier_id(prefix)
{
type: "doc",
content: [
{
type: "paragraph",
content: [
{ text: "#{prefix}-", type: "text" },
{ type: "mention", attrs: DOSSIER_ID_TAG.slice(:id, :label) }
]
}
]
}
end
def suffix(attachment, row_index, index)
suffix = ""
suffix += "-#{add_one_and_pad(row_index)}" if row_index.present?
suffix += "-#{add_one_and_pad(index)}" if index.present?
suffix += attachment.filename.extension_with_delimiter if attachment
suffix
end
def add_one_and_pad(number)
(number + 1).to_s.rjust(2, '0') if number.present?
end
end

View file

@ -1,157 +1,66 @@
class ExportTemplate < ApplicationRecord
include TagsSubstitutionConcern
self.ignored_columns += ["content"]
belongs_to :groupe_instructeur
has_one :procedure, through: :groupe_instructeur
has_many :exports, dependent: :nullify
enum kind: { zip: "zip" }, _prefix: :template
attribute :dossier_folder, :export_item
attribute :export_pdf, :export_item
attribute :pjs, :export_item, array: true
before_validation :ensure_pjs_are_legit
validates_with ExportTemplateValidator
DOSSIER_STATE = Dossier.states.fetch(:en_construction)
FORMAT_DATE = "%Y-%m-%d"
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
# when a pj has been added to a revision, it will not be present in the previous pjs
# a default value is provided.
def pj(tdc)
pjs.find { _1.stable_id == tdc.stable_id } || ExportItem.default_pj(tdc)
end
def tiptap_default_dossier_directory=(body)
self.content["default_dossier_directory"] = JSON.parse(body)
def self.default(name: nil, kind: 'zip', groupe_instructeur:)
dossier_folder = ExportItem.default(prefix: 'dossier')
export_pdf = ExportItem.default(prefix: 'export')
pjs = groupe_instructeur.procedure.exportables_pieces_jointes.map { |tdc| ExportItem.default_pj(tdc) }
new(name:, kind:, groupe_instructeur:, dossier_folder:, export_pdf:, pjs:)
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)
used_tags = TiptapService.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
TiptapService.new.to_path(content_for.deep_symbolize_keys, substitutions)
end
def specific_tags
def tags
tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten
end
def tags_for_pj
specific_tags.push({
def pj_tags
tags.push(
libelle: 'nom original du fichier',
id: 'original-filename',
maybe_null: false
})
id: 'original-filename'
)
end
def attachment_path(dossier, attachment, index: 0, row_index: nil, champ: nil)
file_path = if attachment.name == 'pdf_export_for_instructeur'
export_pdf.path(dossier, attachment:)
elsif attachment.record_type == 'Champ' && pj(champ.type_de_champ).enabled?
pj(champ.type_de_champ).path(dossier, attachment:, index:, row_index:)
else
nil
end
File.join(dossier_folder.path(dossier), file_path) if file_path.present?
end
private
def tiptap_content(key)
content[key]&.to_json
end
def ensure_pjs_are_legit
legitimate_pj_stable_ids = procedure.exportables_pieces_jointes_for_all_versions.map(&:stable_id)
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: 0, row_index: nil, champ: nil)
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'
when 'Attestation', 'Etablissement'
'pieces_justificatives'
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
self.pjs = pjs.filter { _1.stable_id.in?(legitimate_pj_stable_ids) }
end
end

View file

@ -676,6 +676,12 @@ class TypeDeChamp < ApplicationRecord
end
end
def libelle_as_filename
libelle.gsub(/[[:space:]]+/, ' ')
.truncate(30, omission: '', separator: ' ')
.parameterize
end
class << self
def champ_value(type_champ, champ)
dynamic_type_class = type_champ_to_class_name(type_champ).constantize

View file

@ -5,28 +5,26 @@ class PiecesJustificativesService
end
def liste_documents(dossiers)
bill_ids = []
docs = pjs_for_champs(dossiers) +
pjs_for_commentaires(dossiers) +
pjs_for_dossier(dossiers) +
pjs_for_avis(dossiers)
if liste_documents_allows?(:with_bills)
# we do not export bills no more with the new export system
# the bills have never been properly understood by the users
# their export is now deprecated
if liste_documents_allows?(:with_bills) && @export_template.nil?
# 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)
docs += operation_logs
bill_ids += some_bill_ids
end
if liste_documents_allows?(:with_bills)
# then the bills are retrieved without duplication
docs += signatures(bill_ids.uniq)
docs += signatures(some_bill_ids.uniq)
end
docs
docs.filter { |_attachment, path| path.present? }
end
def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s
@ -46,14 +44,14 @@ class PiecesJustificativesService
})
a = ActiveStorage::FakeAttachment.new(
file: StringIO.new(pdf),
filename: "export-#{dossier.id}.pdf",
filename: ActiveStorage::Filename.new("export-#{dossier.id}.pdf"),
name: 'pdf_export_for_instructeur',
id: dossier.id,
created_at: dossier.updated_at
)
if @export_template
pdfs << @export_template.attachment_and_path(dossier, a)
pdfs << [a, @export_template.attachment_path(dossier, a)]
else
pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a)
end
@ -148,7 +146,7 @@ class PiecesJustificativesService
row_index = champs_id_row_index[champ.id]
if @export_template
@export_template.attachment_and_path(champ.dossier, attachment, index:, row_index:, champ:)
[attachment, @export_template.attachment_path(champ.dossier, attachment, index:, row_index:, champ:)]
else
ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment)
end
@ -171,7 +169,7 @@ class PiecesJustificativesService
dossier_id = commentaire_id_dossier_id[a.record_id]
if @export_template
dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a)
[a, @export_template.attachment_path(dossier, a)]
else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end
@ -197,7 +195,7 @@ class PiecesJustificativesService
dossier_id = etablissement_id_dossier_id[a.record_id]
if @export_template
dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a)
[a, @export_template.attachment_path(dossier, a)]
else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end
@ -213,7 +211,7 @@ class PiecesJustificativesService
dossier_id = a.record_id
if @export_template
dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a)
[a, @export_template.attachment_path(dossier, a)]
else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end
@ -234,7 +232,7 @@ class PiecesJustificativesService
dossier_id = attestation_id_dossier_id[a.record_id]
if @export_template
dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a)
[a, @export_template.attachment_path(dossier, a)]
else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end
@ -263,7 +261,7 @@ class PiecesJustificativesService
dossier_id = avis_ids_dossier_id[a.record_id]
if @export_template
dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a)
[a, @export_template.attachment_path(dossier, a)]
else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end

View file

@ -19,10 +19,10 @@ class TiptapService
children(node[:content], substitutions, 0)
end
def to_path(node, substitutions = {})
def to_texts_and_tags(node, substitutions = {})
return '' if node.nil?
children_path(node[:content], substitutions)
children_texts_and_tags(node[:content], substitutions)
end
private
@ -31,18 +31,24 @@ class TiptapService
@body_started = false
end
def children_path(content, substitutions)
content.map { node_to_path(_1, substitutions) }.join
def children_texts_and_tags(content, substitutions)
content.map { node_to_texts_and_tags(_1, substitutions) }.join
end
def node_to_path(node, substitutions)
def node_to_texts_and_tags(node, substitutions)
case node
in type: 'paragraph', content:
children_path(content, substitutions)
in type: 'text', text:, **rest
children_texts_and_tags(content, substitutions)
in type: 'paragraph' # empty paragraph
''
in type: 'text', text:
text.strip
in type: 'mention', attrs: { id: }, **rest
substitutions.fetch(id) { "--#{id}--" }
in type: 'mention', attrs: { id:, label: }
if substitutions.present?
substitutions.fetch(id) { "--#{id}--" }
else
"<span class='fr-tag fr-tag--sm'>#{label}</span>"
end
end
end

View file

@ -0,0 +1,40 @@
class ExportItemType < ActiveRecord::Type::Value
# form_input, or setter -> type
def cast(value)
value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys)
case value
in ExportItem
value
in NilClass # default value
nil
# from db
in { template: Hash, enabled: TrueClass | FalseClass } => h
ExportItem.new(**h.slice(:template, :enabled, :stable_id))
# from form
in { template: String } => h
template = JSON.parse(h[:template]).deep_symbolize_keys
enabled = h[:enabled] == 'true'
stable_id = h[:stable_id]&.to_i
ExportItem.new(template:, enabled:, stable_id:)
end
end
# db -> ruby
def deserialize(value) = cast(value&.then { JSON.parse(_1) })
# ruby -> db
def serialize(value)
if value.is_a?(ExportItem)
JSON.generate({
template: value.template,
enabled: value.enabled,
stable_id: value.stable_id
}.compact)
else
raise ArgumentError, "Invalid value for ExportItem serialization: #{value}"
end
end
end

View file

@ -1,54 +1,66 @@
class ExportTemplateValidator < ActiveModel::Validator
def validate(record)
validate_default_dossier_directory(record)
validate_pdf_name(record)
validate_pjs(record)
def validate(export_template)
validate_all_templates(export_template)
return if export_template.errors.any? # no need to continue if the templates are invalid
validate_dossier_folder(export_template)
validate_export_pdf(export_template)
validate_pjs(export_template)
validate_different_templates(export_template)
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
def validate_all_templates(export_template)
[export_template.dossier_folder, export_template.export_pdf, *export_template.pjs].each(&:template_string)
rescue StandardError
export_template.errors.add(:base, :invalid_template)
end
def validate_dossier_folder(export_template)
if !mentions(export_template.dossier_folder.template).include?('dossier_number')
export_template.errors.add(:dossier_folder, :dossier_number_required)
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
def mentions(template)
TiptapService.used_tags_and_libelle_for(template).map(&:first)
end
def validate_export_pdf(export_template)
return if !export_template.export_pdf.enabled?
if export_template.export_pdf.template_string.empty?
export_template.errors.add(:export_pdf, :blank)
end
end
def attribute_content_text(record, attribute)
attribute_content(record, attribute)&.find { |elem| elem["type"] == "text" }&.fetch("text", nil)
end
def validate_pjs(export_template)
libelle_by_stable_ids = pj_libelle_by_stable_id(export_template)
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)
export_template.pjs.filter(&:enabled?).each do |pj|
if pj.template_string.empty?
libelle = libelle_by_stable_ids[pj.stable_id]
export_template.errors.add(libelle, I18n.t(:blank, scope: 'errors.messages'))
end
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
def validate_different_templates(export_template)
templates = [export_template.export_pdf, *export_template.pjs]
.filter(&:enabled?)
.map(&:template_string)
return if templates.uniq.size == templates.size
export_template.errors.add(:base, :different_templates)
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
def pj_libelle_by_stable_id(export_template)
export_template.procedure.exportables_pieces_jointes
.pluck(:stable_id, :libelle).to_h
end
end

View file

@ -0,0 +1,26 @@
.card.no-list
= hidden_field_tag("#{prefix}[stable_id]", item.stable_id)
.fr-checkbox-group{ data: { controller: 'hide-target' } }
- id = sanitize_to_id("#{prefix}_#{item.stable_id}_enabled")
= check_box_tag "#{prefix}[enabled]", true, item.enabled?, id:, data: { 'hide-target_target': 'source' }
= label_tag id, libelle, class: 'fr-label'
%div{ class: class_names('fr-hidden': !item.enabled?), data: { hide_target_target: 'toHide' } }
%div{ data: { controller: 'hide-target tiptap-to-template'} }
.fr-mt-2w{ data: { hide_target_target: 'toHide' } }
%span Nom du fichier :
%span{ data: { 'tiptap-to-template_target': 'output'} }= sanitize(item.template_string)
.fr-mt-2w
%button.fr-btn.fr-btn--tertiary.fr-btn--sm{ type: 'button', data: { 'hide-target_target': 'source' } } Renommer le fichier
.fr-mt-2w.fr-hidden{ data: { controller: 'tiptap', 'tiptap-attributes-value': { spellcheck: false }.to_json, hide_target_target: 'toHide' } }
%span Renommer le fichier :
.fr-mt-2w.tiptap-editor{ data: { tiptap_target: 'editor' } }
= hidden_field_tag "#{prefix}[template]", item.template_json, data: { tiptap_target: 'input' }, id: nil
.fr-mt-2w
%span.fr-text--sm Cliquez sur les étiquettes que vous souhaitez intégrer au nom du fichier
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.pj_tags })
= button_tag "Valider", type: 'button', class: 'fr-btn fr-mt-2w', data: { 'tiptap-to-template_target': 'trigger', 'hide-target_target': 'source'}

View file

@ -1,3 +1,5 @@
- procedure = @export_template.procedure
#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|
@ -7,72 +9,82 @@
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|
.fr-grid-row.fr-grid-row--gutters
.fr-col-12.fr-col-md-8.fr-pr-4w
= form_with model: [:instructeur, procedure, export_template], data: { turbo: 'true', controller: 'autosubmit' } do |f|
%input.hidden{ type: 'submit', formaction: preview_instructeur_procedure_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'get' }
= render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
= f.hidden_field :kind, value: 'zip'
- if groupe_instructeurs.many?
.fr-input-group
= render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
.fr-input-group{ class: class_names('fr-hidden': groupe_instructeurs.one?) }
= 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 ?
= "#{ExportTemplate.human_attribute_name('groupe_instructeur_id')} #{asterisk}"
%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', 'tiptap-attributes-value': { spellcheck: false }.to_json } }
= f.label '[dossier_folder][template]', class: "fr-label" do
= "#{ExportTemplate.human_attribute_name('dossier_folder')} #{asterisk}"
%span.fr-hint-text Nom du répertoire contenant les différents fichiers à exporter
.tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
= f.hidden_field "[dossier_folder][template]", data: { tiptap_target: 'input' }, value: export_template.dossier_folder.template_json
= f.hidden_field "[dossier_folder][enabled]", value: 'true'
.fr-mt-2w
%span.fr-text--sm Cliquez sur les étiquettes que vous souhaitez intégrer au nom du fichier
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => export_template.tags })
.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')
= render Dsfr::NoticeComponent.new(data_attributes: { class: 'fr-my-4w' }) do |c|
- c.with_title do
Sélectionnez les fichiers que vous souhaitez exporter
.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 })
%h3 Dossier au format PDF
= render partial: 'export_item',
locals: { item: export_template.export_pdf,
libelle: ExportTemplate.human_attribute_name(:export_pdf),
prefix: 'export_template[export_pdf]' }
.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 procedure.exportables_pieces_jointes_for_all_versions.any?
%h3 Pièces justificatives
- if @all_pj.any?
%h3 Pieces justificatives
- procedure.exportables_pieces_jointes.each do |tdc|
- item = export_template.pj(tdc)
= render partial: 'export_item',
locals: { item:,
libelle: tdc.libelle,
prefix: 'export_template[pjs][]'}
.fr-highlight
%p.fr-text--sm
N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes.
- outdated_tdcs = procedure.outdated_exportables_pieces_jointes
- outdated_stable_ids = outdated_tdcs.map(&:stable_id)
- expanded = export_template.pjs.filter(&:enabled?).any? { _1.stable_id.in?(outdated_stable_ids) }
- @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 })
- if outdated_tdcs.any?
%section.fr-accordion.fr-mb-3w
%h3.fr-accordion__title
%button.fr-accordion__btn{ "aria-controls" => "accordion-106", "aria-expanded" => expanded.to_s, "type" => "button" }
pièces justificatives uniquement présentes dans les versions précédentes
.fr-collapse#accordion-106
- outdated_tdcs.each do |tdc|
- item = export_template.pj(tdc)
= render partial: 'export_item',
locals: { item:,
libelle: tdc.libelle,
prefix: 'export_template[pjs][]'}
.fixed-footer
.fr-container
%ul.fr-btns-group.fr-btns-group--inline-md
%li= f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' }
%li= link_to "Annuler", [:exports, :instructeur, procedure], class: "fr-btn fr-btn--secondary"
- if export_template.persisted?
%li
= link_to "Supprimer",
[:instructeur, procedure, export_template],
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"
.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 }
= render partial: 'preview', locals: { export_template: }

View file

@ -1,17 +1,33 @@
- procedure = export_template.procedure
- dossier = procedure.dossier_for_preview(current_instructeur)
#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
- if dossier.nil?
%p.fr-text--sm
Pour générer un aperçu fidèle avec tous les champs et les dates,
= link_to 'créez-vous un dossier', commencer_url(procedure.path), target: '_blank'
et acceptez-le : laperçu lutilisera.
- else
%ul.tree.fr-text--sm
%li
%span.fr-icon-folder-zip-line
#{DownloadableFileService::EXPORT_DIRNAME}/
%li
%ul
%li
%span.fr-icon-folder-line
#{export_template.dossier_folder.path(dossier)}/
%ul
- if export_template.export_pdf.enabled?
%li
%span.fr-icon-pdf-2-line
#{export_template.export_pdf.path(dossier)}.pdf
- procedure.exportables_pieces_jointes.each do |tdc|
- export_pj = export_template.pj(tdc)
- if export_pj.enabled?
%li
%span.fr-icon-file-image-line
#{export_pj.path(dossier)}-1.jpg

View file

@ -4,4 +4,4 @@
.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 }
= render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }

View file

@ -3,4 +3,4 @@
[t('.title')]] }
.fr-container
%h1 Nouveau modèle d'export
= render partial: 'form', locals: { form_url: instructeur_export_templates_path, groupe_instructeurs: @groupe_instructeurs }
= render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs }

View file

@ -38,9 +38,9 @@
%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= link_to export_template.name, [:edit, :instructeur, @procedure, export_template]
%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
= link_to [:new, :instructeur, @procedure, :export_template], class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do
Ajouter un modèle d'export

View file

@ -0,0 +1,5 @@
require Rails.root.join("app/types/export_item_type")
ActiveSupport.on_load(:active_record) do
ActiveRecord::Type.register(:export_item, ExportItemType)
end

View file

@ -6,12 +6,14 @@ en:
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?"
dossier_folder: "How would you like to name the directory containing the documents of a folder?"
export_pdf: "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"
dossier_folder: "Directory's name for pdf format"
export_pdf: "File in pdf format"
errors:
models:
export_template:
dossier_number_mandatory: "must contain dossier's number"
dossier_number_required: "must contain dossier's number"
different_templates: "Files must have different names"
invalid_template: "A file name is invalid"

View file

@ -6,12 +6,14 @@ fr:
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 ?"
dossier_folder: "Comment souhaitez-vous nommer le répertoire contenant les documents d'un dossier ?"
export_pdf: "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"
dossier_folder: Nom du répertoire
export_pdf: "Dossier au format pdf"
errors:
models:
export_template:
dossier_number_mandatory: doit contenir le numéro du dossier
dossier_number_required: doit contenir le numéro du dossier
different_templates: Les fichiers doivent avoir des noms différents
invalid_template: Un nom de fichier est invalide

View file

@ -443,14 +443,17 @@ Rails.application.routes.draw do
#
scope module: 'instructeurs', as: 'instructeur' do
resources :procedures, only: [] do
resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do
collection do
get 'preview'
end
end
end
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

View file

@ -0,0 +1,15 @@
class AddDossierFolderColumnToExportTemplate < ActiveRecord::Migration[7.0]
def up
execute "DELETE FROM export_templates;"
add_column :export_templates, :dossier_folder, :jsonb, default: nil, null: false
add_column :export_templates, :export_pdf, :jsonb, default: nil, null: false
add_column :export_templates, :pjs, :jsonb, array: true, default: [], null: false
end
def down
remove_column :export_templates, :dossier_folder
remove_column :export_templates, :export_pdf
remove_column :export_templates, :pjs
end
end

View file

@ -599,9 +599,12 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_16_091043) do
create_table "export_templates", force: :cascade do |t|
t.jsonb "content", default: {}
t.datetime "created_at", null: false
t.jsonb "dossier_folder", null: false
t.jsonb "export_pdf", null: false
t.bigint "groupe_instructeur_id", null: false
t.string "kind", null: false
t.string "name", null: false
t.jsonb "pjs", default: [], null: false, array: true
t.datetime "updated_at", null: false
t.index ["groupe_instructeur_id"], name: "index_export_templates_on_groupe_instructeur_id"
end

View file

@ -1,58 +1,31 @@
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,
tiptap_pj_3: {
"type" => "doc",
"content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
}.to_json,
tiptap_pj_5: {
"type" => "doc",
"content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
}.to_json,
tiptap_pj_10: {
"type" => "doc",
"content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
}.to_json
}
end
let(:instructeur) { create(:instructeur) }
let(:procedure) do
create(
:procedure, instructeurs: [instructeur],
types_de_champ_public: [
{ type: :piece_justificative, libelle: "pj1", stable_id: 3 },
{ type: :piece_justificative, libelle: "pj2", stable_id: 5 },
{ type: :piece_justificative, libelle: "pj3", stable_id: 10 }
]
types_de_champ_public: [{ type: :piece_justificative, libelle: "pj1", stable_id: 3 }]
)
end
let(:groupe_instructeur) { procedure.defaut_groupe_instructeur }
let(:groupe_instructeur_id) { groupe_instructeur.id }
let(:export_template_params) do
{
name: "coucou",
kind: "zip",
groupe_instructeur_id:,
export_pdf:,
dossier_folder: item_params(text: "DOSSIER_"),
pjs: [pj_item_params(stable_id: 3, text: "avis-commission-"), pj_item_params(stable_id: 666, text: "evil-hack")]
}
end
let(:export_pdf) { item_params(text: "mon_export_") }
describe '#new' do
let(:subject) { get :new, params: { procedure_id: procedure.id } }
subject { get :new, params: { procedure_id: procedure.id } }
it do
subject
@ -61,18 +34,22 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
end
describe '#create' do
let(:subject) { post :create, params: { procedure_id: procedure.id, export_template: export_template_params } }
let(:create_params) { export_template_params }
subject { post :create, params: { procedure_id: procedure.id, export_template: create_params } }
context 'with valid params' do
it 'redirect to some page' do
subject
expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:))
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 }
let(:export_pdf) do
item_params(text: 'toto').merge("template" => { "content" => [{ "content" => "invalid" }] }.to_json)
end
it 'display error notification' do
subject
expect(flash.alert).to be_present
@ -81,16 +58,37 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
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 } }
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
context 'with invalid groupe_instructeur_id' do
let(:groupe_instructeur_id) { create(:groupe_instructeur).id }
it 'display error notification' do
expect { subject }.not_to change(ExportTemplate, :count)
expect(flash.alert).to be_present
end
end
context 'without pjs' do
let(:create_params) { export_template_params.tap { _1.delete(:pjs) } }
it 'works' do
subject
expect(flash.notice).to eq "Le modèle d'export coucou a bien été créé"
expect(ExportTemplate.last.pjs).to match_array([])
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 } }
subject { get :edit, params: { procedure_id: procedure.id, id: export_template.id } }
it 'render edit' do
subject
@ -109,42 +107,53 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
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(:export_pdf) { item_params(text: "exPort_") }
let(:subject) { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } }
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(response).to redirect_to(exports_instructeur_procedure_path(procedure))
expect(flash.notice).to eq "Le modèle d'export coucou a bien été modifié"
export_template.reload
expect(export_template.export_pdf.template_json).to eq(item_params(text: "exPort_")["template"])
expect(export_template.pjs.map(&:template_json)).to eq([item_params(text: "avis-commission-")["template"]])
end
end
context 'with invalid params' do
let(:tiptap_pdf_name) { { content: "invalid" }.to_json }
let(:export_pdf) do
item_params(text: 'a').merge("template" => { "content" => [{ "content" => "invalid" }] }.to_json)
end
it 'display error notification' do
subject
expect(flash.alert).to be_present
end
end
context 'with invalid groupe_instructeur_id' do
let(:groupe_instructeur_id) { create(:groupe_instructeur).id }
it 'display error notification' do
subject
expect(export_template.export_pdf.template_json).not_to eq(item_params(text: "exPort_")["template"])
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 } }
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(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
@ -155,7 +164,7 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
let(:export_template) { create(:export_template, groupe_instructeur:) }
let(:subject) { get :preview, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params }, format: :turbo_stream }
subject { get :preview, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params }, format: :turbo_stream }
it '' do
dossier = create(:dossier, procedure: procedure, for_procedure_preview: true)
@ -164,4 +173,30 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
expect(response.body).to include "mon_export_#{dossier.id}.pdf"
end
end
def pj_item_params(stable_id:, text:, enabled: true)
item_params(text: text, enabled: enabled).merge("stable_id" => stable_id.to_s)
end
def item_params(text:, enabled: true)
{
"enabled" => enabled,
"template" => {
"type" => "doc",
"content" => content(text:)
}.to_json
}
end
def content(text:)
[
{
"type" => "paragraph",
"content" => [
{ "text" => text, "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }
]
}
]
end
end

View file

@ -2,86 +2,11 @@ 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" }
initialize_with { ExportTemplate.default(name:, groupe_instructeur: groupe_instructeur) }
to_create do |export_template, _context|
export_template.set_default_values
export_template.save
end
trait :with_custom_content do
to_create do |export_template, context|
export_template.set_default_values
export_template.content = context.content
export_template.save
end
end
trait :with_custom_ddd_prefix do
transient do
ddd_prefix { 'dossier_' }
end
to_create do |export_template, context|
export_template.set_default_values
export_template.content["default_dossier_directory"]["content"] = [
{
"type" => "paragraph",
"content" =>
[
{ "text" => context.ddd_prefix, "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
{ "text" => " ", "type" => "text" }
]
}
]
export_template.save
end
end
trait :with_date_depot_for_export_pdf do
to_create do |export_template, _|
export_template.set_default_values
export_template.content["pdf_name"]["content"] = [
{
"type" => "paragraph",
"content" =>
[
{ "text" => "export_", "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
{ "text" => "-", "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "dossier_depose_at", "label" => "date de dépôt" } },
{ "text" => " ", "type" => "text" }
]
}
]
export_template.save
trait :enabled_pjs do
after(:build) do |export_template, _evaluator|
export_template.pjs.each { _1.instance_variable_set('@enabled', true) }
end
end
end

View file

@ -1,50 +1,78 @@
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
describe 'public_wrapped_partionned_pjs and exportables_pieces_jointes' do
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) { procedure.active_revision.types_de_champ }
def find_by_stable_id(stable_id) = types_de_champ.find { _1.stable_id == stable_id }
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(: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(: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(: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) }
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) }
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
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 having conditional" do
expect(procedure.public_wrapped_partionned_pjs.second).to match_array([[pjcond]])
end
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 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))
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
it "returns the same list but for all versions" do
expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle))
end
end
end
describe '#outdated_exportables_pieces_jointes' do
let(:types_de_champ_public) do
[
{ type: :piece_justificative, libelle: "outdated", stable_id: 1 },
{ type: :piece_justificative, libelle: "kept", stable_id: 2 }
]
end
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
before do
procedure.draft_revision.remove_type_de_champ(1)
procedure.draft_revision.add_type_de_champ(type_champ: :piece_justificative, libelle: 'new', mandatory: false)
procedure.publish_revision!
end
it { expect(procedure.exportables_pieces_jointes_for_all_versions.map(&:libelle)).to eq(["new", "kept", "outdated"]) }
it { expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array(["kept", "new"]) }
it { expect(procedure.outdated_exportables_pieces_jointes.map(&:libelle)).to match_array(["outdated"]) }
end
end

View file

@ -0,0 +1,20 @@
describe ExportItem do
describe 'path' do
let(:export_item) { ExportItem.default(prefix: 'custom') }
let(:dossier) { create(:dossier) }
let(:attachment) do
ActiveStorage::Attachment.new(
name: 'filename',
blob: ActiveStorage::Blob.new(filename: "file.pdf")
)
end
context 'without index nor row_index' do
it do
expect(export_item.path(dossier, attachment:)).to eq("custom-#{dossier.id}.pdf")
expect(export_item.path(dossier, attachment:, index: 3)).to eq("custom-#{dossier.id}-04.pdf")
expect(export_item.path(dossier, attachment:, row_index: 2, index: 3)).to eq("custom-#{dossier.id}-03-04.pdf")
end
end
end
end

View file

@ -110,9 +110,10 @@ RSpec.describe Export, type: :model do
end
context 'with export template' do
let(:export_template) { build(:export_template) }
let(:export_template) { create(:export_template, groupe_instructeur: gi_1) }
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) }
expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, 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

View file

@ -1,8 +1,7 @@
describe ExportTemplate do
let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) }
let(:export_template) { create(:export_template, :with_custom_content, groupe_instructeur:, content:) }
let(:procedure) { create(:procedure_with_dossiers, types_de_champ_public:, for_individual:) }
let(:dossier) { procedure.dossiers.first }
let(:export_template) { build(:export_template, groupe_instructeur:) }
let(:procedure) { create(:procedure, types_de_champ_public:, for_individual:) }
let(:for_individual) { false }
let(:types_de_champ_public) do
[
@ -10,335 +9,80 @@ describe ExportTemplate do
{ 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) }
describe '.default' do
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 }] }] }
}
]
})
expect(export_template.export_pdf).to eq(ExportItem.default(prefix: "export", enabled: true))
expect(export_template.dossier_folder).to eq(ExportItem.default(prefix: "dossier", enabled: true))
expect(export_template.pjs).to eq([ExportItem.default(stable_id: 3, prefix: "justificatif-de-domicile", enabled: false)])
end
end
describe '#assign_pj_names' do
let(:pj_params) do
{
"tiptap_pj_1" => {
"type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
}.to_json
}
describe '#pj' do
context 'when pj exists' do
subject { export_template.pj(double(stable_id: 3)) }
it { is_expected.to eq(ExportItem.default(stable_id: 3, prefix: "justificatif-de-domicile", enabled: false)) }
end
it 'values content from pj params' do
export_template.assign_pj_names(pj_params)
expect(export_template.content["pjs"]).to eq [
{ :path => { "content" => [{ "content" => [{ "text" => "avis-commission-", "type" => "text" }, { "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" }, "type" => "mention" }], "type" => "paragraph" }], "type" => "doc" }, :stable_id => "1" }
]
context 'when pj does not exist' do
subject { export_template.pj(TypeDeChamp.new(libelle: 'hi', stable_id: 10)) }
it { is_expected.to eq(ExportItem.default(stable_id: 10, prefix: "hi", enabled: false)) }
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) }
describe '#attachment_path' do
let(:dossier) { create(:dossier, :en_construction, procedure:) }
context 'for export pdf' do
let(:attachment) { double("attachment") }
let(:export_template) do
build(:export_template, groupe_instructeur:, dossier_folder: ExportItem.default(prefix: "DOSSIER"), export_pdf: ExportItem.default(prefix: "mon_export"))
end
let(:attachment) { ActiveStorage::Attachment.new(name: 'pdf_export_for_instructeur', blob: ActiveStorage::Blob.new(filename: "export.pdf")) }
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"])
expect(export_template.attachment_path(dossier, attachment)).to eq("DOSSIER-#{dossier.id}/mon_export-#{dossier.id}.pdf")
end
end
context 'for pj' do
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
let(:champ_pj) { dossier.champs.find(&:piece_justificative?) }
let(:champ_pj) { dossier.champs_public.first }
let(:export_template) { create(:export_template, groupe_instructeur:, pjs: [ExportItem.default(stable_id: 3, prefix: "justif", enabled: true)]) }
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
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) { create(:procedure, :for_individual, types_de_champ_public:) }
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
let(:draft) { procedure.draft_revision }
let(:types_de_champ_public) do
[
{
type: :repetition,
stable_id: 3333,
mandatory: true, children: [
{ type: :text, libelle: 'sub type de champ' },
{ type: :piece_justificative, stable_id: 10, libelle: 'pj repet' }
]
}
]
end
let(:champ_pj) { dossier.champs.find(&:piece_justificative?) }
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
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"])
expect(export_template.attachment_path(dossier, attachment, champ: champ_pj)).to eq("dossier-#{dossier.id}/justif-#{dossier.id}-01.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
describe '#tags and #pj_tags' do
let(:procedure) { build(:procedure, for_individual:) }
it 'convert pdf_name' do
expect(export_template.tiptap_convert(procedure.dossiers.first, "pdf_name")).to eq "mon_export_#{dossier.id}"
end
context 'for date' do
let(:export_template) { create(:export_template, :with_date_depot_for_export_pdf, groupe_instructeur:) }
let(:dossier) { create(:dossier, :en_construction, procedure:, depose_at: Date.parse("2024/03/30")) }
it 'convert date with dash' do
expect(export_template.tiptap_convert(dossier, "pdf_name")).to eq "export_#{dossier.id}-2024-03-30"
end
end
end
describe '#tiptap_convert_pj' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile' }]) }
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
let(:champ_pj) { dossier.champs.first }
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, 3, 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
context 'for entreprise procedure' do
let(:for_individual) { false }
let(:expected_tags) do
['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
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
context 'for entreprise procedure' do
let(:for_individual) { false }
describe 'specific_tags' do
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"]
expect(export_template.tags.map { _1[:id] }).to eq(expected_tags)
expect(export_template.pj_tags.map { _1[:id] }).to eq(expected_tags + ['original-filename'])
end
end
describe 'tags_for_pj' do
it do
tags = export_template.tags_for_pj
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", "original-filename"]
context 'for individual procedure' do
let(:for_individual) { true }
let(:expected_tags) do
['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
context 'for individual procedure' do
let(:for_individual) { true }
describe 'specific_tags' do
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
describe 'tags_for_pj' do
it do
tags = export_template.tags_for_pj
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", "original-filename"]
expect(export_template.tags.map { _1[:id] }).to eq(expected_tags)
expect(export_template.pj_tags.map { _1[:id] }).to eq(expected_tags + ['original-filename'])
end
end
end

View file

@ -302,4 +302,12 @@ describe TypeDeChamp do
it { expect(create(:type_de_champ, :header_section, libelle: " 2.3 Test").libelle).to eq("2.3 Test") }
it { expect(create(:type_de_champ, libelle: " fix me ").libelle).to eq("fix me") }
end
describe '#safe_filename' do
subject { build(:type_de_champ, libelle:).libelle_as_filename }
let(:libelle) { " #/🐉 1 très intéressant Bilan " }
it { is_expected.to eq("1-tres-interessant-bilan") }
end
end

View file

@ -25,7 +25,7 @@ describe PiecesJustificativesService do
before { attach_file_to_champ(champ) }
it do
expect(export_template).to receive(:attachment_and_path)
expect(export_template).to receive(:attachment_path)
.with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:)
subject
end
@ -40,10 +40,10 @@ describe PiecesJustificativesService do
end
it do
expect(export_template).to receive(:attachment_and_path)
expect(export_template).to receive(:attachment_path)
.with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:)
expect(export_template).to receive(:attachment_and_path)
expect(export_template).to receive(:attachment_path)
.with(dossier, attachments(pj_champ(dossier)).second, index: 1, row_index: nil, champ:)
subject
end
@ -66,13 +66,13 @@ describe PiecesJustificativesService 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)
expect(export_template).to receive(:attachment_path)
.with(dossier, first_child_attachments.first, index: 0, row_index: 0, champ: first_champ)
expect(export_template).to receive(:attachment_and_path)
expect(export_template).to receive(:attachment_path)
.with(dossier, first_child_attachments.second, index: 1, row_index: 0, champ: first_champ)
expect(export_template).to receive(:attachment_and_path)
expect(export_template).to receive(:attachment_path)
.with(dossier, second_child_attachments.first, index: 0, row_index: 1, champ: second_champ)
count = 0
@ -90,6 +90,7 @@ describe PiecesJustificativesService do
describe '.liste_documents' do
let(:dossier) { create(:dossier, procedure: procedure) }
let(:dossiers) { Dossier.where(id: dossier.id) }
let(:default_export_template) { build(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) }
let(:export_template) { nil }
subject do
PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:first)
@ -99,39 +100,39 @@ describe PiecesJustificativesService do
let(:user_profile) { build(:administrateur) }
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative }]) }
let(:witness) { create(:dossier, procedure: procedure) }
let(:pj_champ) { -> (d) { d.champs_public.find { |c| c.type == 'Champs::PieceJustificativeChamp' } } }
def pj_champ(d) = d.champs_public.find { |c| c.type == 'Champs::PieceJustificativeChamp' }
context 'with a single attachment' do
before do
attach_file_to_champ(pj_champ.call(dossier))
attach_file_to_champ(pj_champ.call(witness))
attach_file_to_champ(pj_champ(dossier))
attach_file_to_champ(pj_champ(witness))
end
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
it { expect(subject).to match_array(pj_champ(dossier).piece_justificative_file.attachments) }
context 'with export_template' do
let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) }
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
let(:export_template) { build(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) }
it { expect(subject).to match_array(pj_champ(dossier).piece_justificative_file.attachments) }
end
end
context 'with a multiple attachments' do
before do
attach_file_to_champ(pj_champ.call(dossier))
attach_file_to_champ(pj_champ.call(witness))
attach_file_to_champ(pj_champ.call(dossier))
attach_file_to_champ(pj_champ(dossier))
attach_file_to_champ(pj_champ(witness))
attach_file_to_champ(pj_champ(dossier))
end
it { expect(subject.count).to eq(2) }
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
it { expect(subject).to match_array(pj_champ(dossier).piece_justificative_file.attachments) }
end
context 'with a pj not safe on a champ' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative }]) }
let(:dossier) { create(:dossier, procedure: procedure) }
let(:pj_champ) { -> (d) { d.champs_public.find { |c| c.type == 'Champs::PieceJustificativeChamp' } } }
before { attach_file_to_champ(pj_champ.call(dossier), safe = false) }
before { attach_file_to_champ(pj_champ(dossier), false) }
it { expect(subject).to be_empty }
end
@ -139,7 +140,6 @@ describe PiecesJustificativesService do
context 'with a identite champ pj' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :titre_identite }]) }
let(:dossier) { create(:dossier, procedure: procedure) }
let(:witness) { create(:dossier, procedure: procedure) }
let(:champ_identite) { dossier.champs_public.find { |c| c.type == 'Champs::TitreIdentiteChamp' } }
@ -149,6 +149,12 @@ describe PiecesJustificativesService do
expect(champ_identite.piece_justificative_file).to be_attached
expect(subject).to be_empty
end
context 'with export_template' do
let(:export_template) { build(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) }
it { expect(subject).to be_empty }
end
end
context 'with a pj on an commentaire' do
@ -166,10 +172,9 @@ describe PiecesJustificativesService do
it { expect(subject).to match_array(dossier.commentaires.first.piece_jointe.attachments) }
context 'with export_template' do
let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
it 'uses specific name for dossier directory' do
expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/messagerie")).to be true
end
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
end
@ -177,7 +182,7 @@ describe PiecesJustificativesService do
let(:dossier) { create(:dossier) }
let!(:commentaire) { create(:commentaire, dossier: dossier) }
before { attach_file(commentaire.piece_jointe, safe = false) }
before { attach_file(commentaire.piece_jointe, false) }
it { expect(subject).to be_empty }
end
@ -189,17 +194,16 @@ describe PiecesJustificativesService do
it { expect(subject).to match_array(dossier.justificatif_motivation.attachment) }
context 'with export_template' do
let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
it 'uses specific name for dossier directory' do
expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/dossier")).to be true
end
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
end
context 'with a motivation not safe' do
let(:dossier) { create(:dossier) }
before { attach_file(dossier.justificatif_motivation, safe = false) }
before { attach_file(dossier.justificatif_motivation, false) }
it { expect(subject).to be_empty }
end
@ -214,10 +218,9 @@ describe PiecesJustificativesService do
end
context 'with export_template' do
let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
it 'uses specific name for dossier directory' do
expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/pieces_justificatives")).to be true
end
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
end
@ -242,10 +245,9 @@ describe PiecesJustificativesService do
end
context 'with export_template' do
let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
it 'uses specific name for dossier directory' do
expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/pieces_justificatives")).to be true
end
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
end
end
@ -256,32 +258,33 @@ describe PiecesJustificativesService do
let(:witness) { create(:dossier, procedure: procedure) }
let!(:private_pj) { create(:type_de_champ_piece_justificative, procedure: procedure, private: true) }
let(:private_pj_champ) { -> (d) { d.champs_private.find { |c| c.type == 'Champs::PieceJustificativeChamp' } } }
def private_pj_champ(d) = d.champs_private.find { |c| c.type == 'Champs::PieceJustificativeChamp' }
before do
attach_file_to_champ(private_pj_champ.call(dossier))
attach_file_to_champ(private_pj_champ.call(witness))
attach_file_to_champ(private_pj_champ(dossier))
attach_file_to_champ(private_pj_champ(witness))
end
context 'given an administrateur' do
let(:user_profile) { build(:administrateur) }
it { expect(subject).to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) }
it { expect(subject).to match_array(private_pj_champ(dossier).piece_justificative_file.attachments) }
end
context 'given an instructeur' do
let(:user_profile) { create(:instructeur) }
it { expect(subject).to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) }
it { expect(subject).to match_array(private_pj_champ(dossier).piece_justificative_file.attachments) }
end
context 'given an expert' do
let(:user_profile) { create(:expert) }
it { expect(subject).not_to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) }
it { expect(subject).not_to match_array(private_pj_champ(dossier).piece_justificative_file.attachments) }
end
end
context 'acl on bill' do
let(:dossier) { create(:dossier) }
let(:witness) { create(:dossier) }
let(:default_export_template) { build(:export_template, groupe_instructeur: dossier.procedure.defaut_groupe_instructeur) }
let(:bill_signature) do
bs = build(:bill_signature, :with_serialized, :with_signature)
@ -309,6 +312,12 @@ describe PiecesJustificativesService do
expect(subject).to match_array([dossier_bs.serialized.attachment, dossier_bs.signature.attachment])
end
context 'with export_template' do
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
context 'with a dol' do
let(:dol) { create(:dossier_operation_log, dossier: dossier) }
let(:witness_dol) { create(:dossier_operation_log, dossier: witness) }
@ -319,6 +328,12 @@ describe PiecesJustificativesService do
end
it { expect(subject).to include(dol.serialized.attachment) }
context 'with export_template' do
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
end
end
@ -361,6 +376,12 @@ describe PiecesJustificativesService do
it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
context 'with export_template' do
let(:export_template) { default_export_template }
it { expect(subject).to be_empty }
end
end
context 'given an instructeur' do
@ -415,13 +436,6 @@ describe PiecesJustificativesService do
it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
context 'with export_template' do
let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
it 'uses specific name for dossier directory' do
expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/avis")).to be true
end
end
end
context 'given an expert' do
@ -464,11 +478,12 @@ describe PiecesJustificativesService do
end
context 'with export template' do
let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
let(:groupe_instructeur) { procedure.defaut_groupe_instructeur }
let(:export_template) { create(:export_template, groupe_instructeur:, dossier_folder: ExportItem.default(prefix: 'DOSSIER')) }
subject { PiecesJustificativesService.new(user_profile:, export_template:).generate_dossiers_export(dossiers) }
it 'gives custom name to export pdf file' do
expect(subject.first.second).to eq "DOSSIER-#{dossier.id}/export_#{dossier.id}.pdf"
expect(subject.first.second).to eq "DOSSIER-#{dossier.id}/export-#{dossier.id}.pdf"
end
end
end

View file

@ -450,7 +450,7 @@ describe ProcedureExportService do
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) }
let(:export_template) { create(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) }
before do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
end
@ -465,10 +465,9 @@ describe ProcedureExportService do
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"
"#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-01.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')

View file

@ -2,7 +2,7 @@ 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) }
let(:export_template) { create(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) }
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp')
@ -49,11 +49,11 @@ describe ProcedureExportService do
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"
"export/dossier-#{dossier.id}/export-#{dossier.id}.pdf",
"export/dossier-#{dossier.id}/pj-#{dossier.id}-01.png",
"export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-01-01.png",
"export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-02-01.png",
"export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-01-02.png"
]
expect(files.size).to eq(dossiers.count * 6 + 1)

View file

@ -193,19 +193,40 @@ RSpec.describe TiptapService do
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" }] }
]
describe '.to_texts_and_tags' do
subject { described_class.new.to_texts_and_tags(json, substitutions) }
}.deep_symbolize_keys
context 'nominal' do
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
context 'with substitutions' do
let(:substitutions) { { "dossier_number" => "42" } }
it 'returns texts_and_tags' do
is_expected.to eq("export_42.pdf")
end
end
context 'without substitutions' do
let(:substitutions) { nil }
it 'returns texts_and_tags' do
is_expected.to eq("export_<span class='fr-tag fr-tag--sm'>numéro du dossier</span>.pdf")
end
end
end
it 'returns path' do
expect(described_class.new.to_path(json, substitutions)).to eq("export_42.pdf")
context 'empty paragraph' do
let(:json) { { content: [{ type: 'paragraph' }] } }
let(:substitutions) { {} }
it { is_expected.to eq('') }
end
end
end

View file

@ -0,0 +1,55 @@
describe ExportItemType do
let(:type) { ExportItemType.new }
describe 'cast' do
it 'from ExportItem' do
export_item = ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42)
expect(type.cast(export_item)).to eq(export_item)
end
it 'from nil' do
expect(type.cast(nil)).to eq(nil)
end
it 'from db' do
h = { template: { foo: 'bar' }, enabled: true, stable_id: 42 }
expect(type.cast(h)).to eq(ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42))
end
it 'from form' do
h = { template: '{"foo":{"bar":"zob"}}' }
expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: false))
h = { template: '{"foo":{"bar":"zob"}}', enabled: 'true' }
expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: true))
h = { template: '{"foo":{"bar":"zob"}}', stable_id: '42' }
expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: false, stable_id: 42))
h = { template: '{"foo":{"bar":"zob"}}', enabled: 'true', stable_id: '42' }
expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: true, stable_id: 42))
end
it 'from invalid value' do
expect { type.cast('invalid value') }.to raise_error(NoMatchingPatternError)
end
end
describe 'deserialize' do
it 'from nil' do
expect(type.deserialize(nil)).to eq(nil)
end
it 'from db' do
h = { template: { foo: 'bar' }, enabled: true, stable_id: 42 }
expect(type.deserialize(JSON.generate(h))).to eq(ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42))
end
end
describe 'serialize' do
it 'from ExportItem' do
export_item = ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42)
expect(type.serialize(export_item)).to eq('{"template":{"foo":"bar"},"enabled":true,"stable_id":42}')
end
end
end

View file

@ -0,0 +1,79 @@
describe ExportTemplateValidator do
let(:validator) { ExportTemplateValidator.new }
describe 'validate' do
let(:exportables_pieces_jointes) { [double('pj', stable_id: 3, libelle: 'libelle')] }
let(:pj_libelle_by_stable_id) { exportables_pieces_jointes.map { |pj| [pj.stable_id, pj.libelle] }.to_h }
def empty_template(enabled: true, stable_id: nil)
{ template: { type: "doc", content: [] }, enabled: enabled, stable_id: stable_id }.compact
end
def errors(export_template) = export_template.errors.map { [_1.attribute, _1.message] }
before do
allow(validator).to receive(:pj_libelle_by_stable_id).and_return(pj_libelle_by_stable_id)
validator.validate(export_template)
end
context 'with a default export template' do
let(:export_template) { build(:export_template) }
it { expect(export_template.errors.count).to eq(0) }
end
context 'with a invalid template' do
let(:export_template) do
export_pdf = { template: { is: 'invalid' }, enabled: true }
build(:export_template, export_pdf:)
end
it { expect(errors(export_template)).to eq([[:base, "Un nom de fichier est invalide"]]) }
end
context 'with a empty export_pdf' do
let(:export_template) { build(:export_template, export_pdf: empty_template) }
it { expect(errors(export_template)).to eq([[:export_pdf, "doit être rempli"]]) }
end
context 'with a empty export_pdf disabled' do
let(:export_template) { build(:export_template, export_pdf: empty_template(enabled: false)) }
it { expect(export_template.errors.count).to eq(0) }
end
context 'with a dossier_folder without dossier_number' do
let(:export_template) do
dossier_folder = ExportItem.default(prefix: 'dossier')
dossier_folder.template[:content][0][:content][1][:attrs][:id] = :other
build(:export_template, dossier_folder:)
end
it { expect(errors(export_template)).to eq([[:dossier_folder, "doit contenir le numéro du dossier"]]) }
end
context 'with a empty pj' do
let(:export_template) { build(:export_template, pjs: [empty_template(stable_id: 3)]) }
it { expect(errors(export_template)).to eq([[:libelle, "doit être rempli"]]) }
end
context 'with a empty pj disabled' do
let(:export_template) { build(:export_template, pjs: [empty_template(enabled: false)]) }
it { expect(export_template.errors.count).to eq(0) }
end
context 'with multiple files bearing the same template' do
let(:export_item) { ExportItem.default(prefix: 'same') }
let(:export_template) do
build(:export_template, export_pdf: export_item, pjs: [export_item])
end
it { expect(errors(export_template)).to eq([[:base, "Les fichiers doivent avoir des noms différents"]]) }
end
end
end