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 { &.no-list {
ul { ul {
list-style: none !important; 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 { &-intermediate-circle-fill {
&:before, &:before,
&:after { &: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 { &-underline {
&:before, &:before,
&:after { &: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 = 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}" = "Exporter à partir du modèle #{export_template.name}"
- menu.with_item do - 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 Ajouter un modèle d'export

View file

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

View file

@ -150,4 +150,6 @@ module ApplicationHelper
.map { |word| word[0].upcase } .map { |word| word[0].upcase }
.join .join
end end
def asterisk = render(EditableChamp::AsteriskMandatoryComponent.new)
end 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 { export class TiptapController extends ApplicationController {
static targets = ['editor', 'input', 'button', 'tag']; static targets = ['editor', 'input', 'button', 'tag'];
static values = { static values = {
insertAfterTag: { type: String, default: '' } insertAfterTag: { type: String, default: '' },
attributes: { type: Object, default: {} }
}; };
declare editorTarget: Element; declare editorTarget: Element;
@ -18,6 +19,7 @@ export class TiptapController extends ApplicationController {
declare buttonTargets: HTMLButtonElement[]; declare buttonTargets: HTMLButtonElement[];
declare tagTargets: HTMLElement[]; declare tagTargets: HTMLElement[];
declare insertAfterTagValue: string; declare insertAfterTagValue: string;
declare attributesValue: Record<string, string>;
#initializing = true; #initializing = true;
#editor?: Editor; #editor?: Editor;
@ -28,6 +30,7 @@ export class TiptapController extends ApplicationController {
content: this.content, content: this.content,
tags: this.tags, tags: this.tags,
buttons: this.menuButtons, buttons: this.menuButtons,
attributes: { class: 'fr-input', ...this.attributesValue },
onChange: ({ editor }) => { onChange: ({ editor }) => {
for (const button of this.buttonTargets) { for (const button of this.buttonTargets) {
const action = getAction(editor, button); 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, content,
tags, tags,
buttons, buttons,
attributes,
onChange onChange
}: { }: {
editorElement: Element; editorElement: Element;
@ -40,8 +41,15 @@ export function createEditor({
tags: TagSchema[]; tags: TagSchema[];
buttons: string[]; buttons: string[];
onChange(change: { editor: Editor }): void; onChange(change: { editor: Editor }): void;
attributes?: Record<string, string>;
}): Editor { }): Editor {
const options = getEditorOptions(editorElement, tags, buttons, content); const options = getEditorOptions(
editorElement,
tags,
buttons,
content,
attributes
);
const editor = new Editor(options); const editor = new Editor(options);
editor.on('transaction', onChange); editor.on('transaction', onChange);
return editor; return editor;
@ -51,7 +59,8 @@ function getEditorOptions(
element: Element, element: Element,
tags: TagSchema[], tags: TagSchema[],
actions: string[], actions: string[],
content?: JSONContent content?: JSONContent,
attributes?: Record<string, string>
): Partial<EditorOptions> { ): Partial<EditorOptions> {
const extensions: Extensions = []; const extensions: Extensions = [];
for (const action of actions) { for (const action of actions) {
@ -123,7 +132,7 @@ function getEditorOptions(
return { return {
element, element,
content, content,
editorProps: { attributes: { class: 'fr-input' } }, editorProps: { attributes },
extensions: [ extensions: [
actions.includes('title') ? DocumentWithHeader : Document, actions.includes('title') ? DocumentWithHeader : Document,
Hystory, Hystory,

View file

@ -11,14 +11,26 @@ module PiecesJointesListConcern
pieces_jointes(exclude_titre_identite: true) pieces_jointes(exclude_titre_identite: true)
end 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 private
def pieces_jointes( def pieces_jointes(
exclude_titre_identite: false, exclude_titre_identite: false,
public_only: 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) .includes(:type_de_champ, revision_types_de_champ: :type_de_champ)
coordinates = coordinates.public_only if public_only 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 class ExportTemplate < ApplicationRecord
include TagsSubstitutionConcern include TagsSubstitutionConcern
self.ignored_columns += ["content"]
belongs_to :groupe_instructeur belongs_to :groupe_instructeur
has_one :procedure, through: :groupe_instructeur has_one :procedure, through: :groupe_instructeur
has_many :exports, dependent: :nullify 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 validates_with ExportTemplateValidator
DOSSIER_STATE = Dossier.states.fetch(:en_construction) DOSSIER_STATE = Dossier.states.fetch(:en_construction)
FORMAT_DATE = "%Y-%m-%d"
def set_default_values # when a pj has been added to a revision, it will not be present in the previous pjs
content["default_dossier_directory"] = tiptap_json("dossier-") # a default value is provided.
content["pdf_name"] = tiptap_json("export_") def pj(tdc)
pjs.find { _1.stable_id == tdc.stable_id } || ExportItem.default_pj(tdc)
content["pjs"] = []
procedure.exportables_pieces_jointes.each do |pj|
content["pjs"] << { "stable_id" => pj.stable_id.to_s, "path" => tiptap_json("#{pj.libelle.parameterize}-") }
end
end end
def tiptap_default_dossier_directory=(body) def self.default(name: nil, kind: 'zip', groupe_instructeur:)
self.content["default_dossier_directory"] = JSON.parse(body) 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 end
def tiptap_default_dossier_directory def tags
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
tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten
end end
def tags_for_pj def pj_tags
specific_tags.push({ tags.push(
libelle: 'nom original du fichier', libelle: 'nom original du fichier',
id: 'original-filename', id: 'original-filename'
maybe_null: false )
}) 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 end
private private
def tiptap_content(key) def ensure_pjs_are_legit
content[key]&.to_json legitimate_pj_stable_ids = procedure.exportables_pieces_jointes_for_all_versions.map(&:stable_id)
end
def tiptap_json(prefix) self.pjs = pjs.filter { _1.stable_id.in?(legitimate_pj_stable_ids) }
{
"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
end end
end end

View file

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

View file

@ -5,28 +5,26 @@ class PiecesJustificativesService
end end
def liste_documents(dossiers) def liste_documents(dossiers)
bill_ids = []
docs = pjs_for_champs(dossiers) + docs = pjs_for_champs(dossiers) +
pjs_for_commentaires(dossiers) + pjs_for_commentaires(dossiers) +
pjs_for_dossier(dossiers) + pjs_for_dossier(dossiers) +
pjs_for_avis(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 # some bills are shared among operations
# so first, all the bill_ids are fetched # so first, all the bill_ids are fetched
operation_logs, some_bill_ids = operation_logs_and_signature_ids(dossiers) operation_logs, some_bill_ids = operation_logs_and_signature_ids(dossiers)
docs += operation_logs docs += operation_logs
bill_ids += some_bill_ids
end
if liste_documents_allows?(:with_bills)
# then the bills are retrieved without duplication # then the bills are retrieved without duplication
docs += signatures(bill_ids.uniq) docs += signatures(some_bill_ids.uniq)
end end
docs docs.filter { |_attachment, path| path.present? }
end end
def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s
@ -46,14 +44,14 @@ class PiecesJustificativesService
}) })
a = ActiveStorage::FakeAttachment.new( a = ActiveStorage::FakeAttachment.new(
file: StringIO.new(pdf), file: StringIO.new(pdf),
filename: "export-#{dossier.id}.pdf", filename: ActiveStorage::Filename.new("export-#{dossier.id}.pdf"),
name: 'pdf_export_for_instructeur', name: 'pdf_export_for_instructeur',
id: dossier.id, id: dossier.id,
created_at: dossier.updated_at created_at: dossier.updated_at
) )
if @export_template if @export_template
pdfs << @export_template.attachment_and_path(dossier, a) pdfs << [a, @export_template.attachment_path(dossier, a)]
else else
pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a) pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a)
end end
@ -148,7 +146,7 @@ class PiecesJustificativesService
row_index = champs_id_row_index[champ.id] row_index = champs_id_row_index[champ.id]
if @export_template 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 else
ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment) ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment)
end end
@ -171,7 +169,7 @@ class PiecesJustificativesService
dossier_id = commentaire_id_dossier_id[a.record_id] dossier_id = commentaire_id_dossier_id[a.record_id]
if @export_template if @export_template
dossier = dossiers.find { _1.id == dossier_id } dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a) [a, @export_template.attachment_path(dossier, a)]
else else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end end
@ -197,7 +195,7 @@ class PiecesJustificativesService
dossier_id = etablissement_id_dossier_id[a.record_id] dossier_id = etablissement_id_dossier_id[a.record_id]
if @export_template if @export_template
dossier = dossiers.find { _1.id == dossier_id } dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a) [a, @export_template.attachment_path(dossier, a)]
else else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end end
@ -213,7 +211,7 @@ class PiecesJustificativesService
dossier_id = a.record_id dossier_id = a.record_id
if @export_template if @export_template
dossier = dossiers.find { _1.id == dossier_id } dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a) [a, @export_template.attachment_path(dossier, a)]
else else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end end
@ -234,7 +232,7 @@ class PiecesJustificativesService
dossier_id = attestation_id_dossier_id[a.record_id] dossier_id = attestation_id_dossier_id[a.record_id]
if @export_template if @export_template
dossier = dossiers.find { _1.id == dossier_id } dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a) [a, @export_template.attachment_path(dossier, a)]
else else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end end
@ -263,7 +261,7 @@ class PiecesJustificativesService
dossier_id = avis_ids_dossier_id[a.record_id] dossier_id = avis_ids_dossier_id[a.record_id]
if @export_template if @export_template
dossier = dossiers.find { _1.id == dossier_id } dossier = dossiers.find { _1.id == dossier_id }
@export_template.attachment_and_path(dossier, a) [a, @export_template.attachment_path(dossier, a)]
else else
ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
end end

View file

@ -19,10 +19,10 @@ class TiptapService
children(node[:content], substitutions, 0) children(node[:content], substitutions, 0)
end end
def to_path(node, substitutions = {}) def to_texts_and_tags(node, substitutions = {})
return '' if node.nil? return '' if node.nil?
children_path(node[:content], substitutions) children_texts_and_tags(node[:content], substitutions)
end end
private private
@ -31,18 +31,24 @@ class TiptapService
@body_started = false @body_started = false
end end
def children_path(content, substitutions) def children_texts_and_tags(content, substitutions)
content.map { node_to_path(_1, substitutions) }.join content.map { node_to_texts_and_tags(_1, substitutions) }.join
end end
def node_to_path(node, substitutions) def node_to_texts_and_tags(node, substitutions)
case node case node
in type: 'paragraph', content: in type: 'paragraph', content:
children_path(content, substitutions) children_texts_and_tags(content, substitutions)
in type: 'text', text:, **rest in type: 'paragraph' # empty paragraph
''
in type: 'text', text:
text.strip text.strip
in type: 'mention', attrs: { id: }, **rest in type: 'mention', attrs: { id:, label: }
if substitutions.present?
substitutions.fetch(id) { "--#{id}--" } substitutions.fetch(id) { "--#{id}--" }
else
"<span class='fr-tag fr-tag--sm'>#{label}</span>"
end
end 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 class ExportTemplateValidator < ActiveModel::Validator
def validate(record) def validate(export_template)
validate_default_dossier_directory(record) validate_all_templates(export_template)
validate_pdf_name(record)
validate_pjs(record) 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 end
private private
def validate_default_dossier_directory(record) def validate_all_templates(export_template)
mention = attribute_content_mention(record, :default_dossier_directory) [export_template.dossier_folder, export_template.export_pdf, *export_template.pjs].each(&:template_string)
if mention&.fetch("id", nil) != "dossier_number"
record.errors.add :tiptap_default_dossier_directory, :dossier_number_mandatory 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
end end
def validate_pdf_name(record) def mentions(template)
if attribute_content_text(record, :pdf_name).blank? && attribute_content_mention(record, :pdf_name).blank? TiptapService.used_tags_and_libelle_for(template).map(&:first)
record.errors.add :tiptap_pdf_name, :blank 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
end end
def attribute_content_text(record, attribute) def validate_pjs(export_template)
attribute_content(record, attribute)&.find { |elem| elem["type"] == "text" }&.fetch("text", nil) libelle_by_stable_ids = pj_libelle_by_stable_id(export_template)
end
def attribute_content_mention(record, attribute) export_template.pjs.filter(&:enabled?).each do |pj|
attribute_content(record, attribute)&.find { |elem| elem["type"] == "mention" }&.fetch("attrs", nil) 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
def attribute_content(record, attribute)
content = record.content[attribute.to_s]&.fetch("content", nil)
if content.is_a?(Array)
content.first&.fetch("content", nil)
end end
end end
def validate_pjs(record) def validate_different_templates(export_template)
record.content["pjs"]&.each do |pj| templates = [export_template.export_pdf, *export_template.pjs]
pj_sym = pj.symbolize_keys .filter(&:enabled?)
libelle = record.groupe_instructeur.procedure.exportables_pieces_jointes.find { _1.stable_id.to_s == pj_sym[:stable_id] }&.libelle&.to_sym .map(&:template_string)
validate_content(record, pj_sym[:path], libelle)
end return if templates.uniq.size == templates.size
export_template.errors.add(:base, :different_templates)
end end
def validate_content(record, attribute_content, attribute) def pj_libelle_by_stable_id(export_template)
if attribute_content.nil? || attribute_content["content"].nil? || export_template.procedure.exportables_pieces_jointes
attribute_content["content"].first.nil? || .pluck(:stable_id, :libelle).to_h
attribute_content["content"].first["content"].nil? ||
(attribute_content["content"].first["content"].find { |elem| elem["text"].blank? } && attribute_content["content"].first["content"].find { |elem| elem["type"] == "mention" }["attrs"].blank?)
record.errors.add attribute, I18n.t(:blank, scope: 'errors.messages')
end
end end
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 #export_template-edit.fr-my-4w
.fr-mb-6w .fr-mb-6w
= render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c| = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c|
@ -8,71 +10,81 @@
en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}. en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}.
.fr-grid-row.fr-grid-row--gutters .fr-grid-row.fr-grid-row--gutters
.fr-col-12.fr-col-md-8 .fr-col-12.fr-col-md-8.fr-pr-4w
= form_with url: form_url, model: @export_template, local: true, data: { turbo: 'true', controller: 'autosubmit' } do |f| = 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' }
= f.hidden_field :kind, value: 'zip'
= render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field)
- if groupe_instructeurs.many? .fr-input-group{ class: class_names('fr-hidden': groupe_instructeurs.one?) }
.fr-input-group
= f.label :groupe_instructeur_id, class: 'fr-label' do = f.label :groupe_instructeur_id, class: 'fr-label' do
= f.object.class.human_attribute_name(:groupe_instructeur_id) = "#{ExportTemplate.human_attribute_name('groupe_instructeur_id')} #{asterisk}"
= render EditableChamp::AsteriskMandatoryComponent.new %span.fr-hint-text Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ?
%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' = f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select'
- else
= f.hidden_field :groupe_instructeur_id
= f.hidden_field :kind
.fr-input-group{ data: { controller: 'tiptap' } }
= f.label :tiptap_default_dossier_directory, class: "fr-label" do
= f.object.class.human_attribute_name(:tiptap_default_dossier_directory)
= render EditableChamp::AsteriskMandatoryComponent.new
%span.fr-hint-text
= t('activerecord.attributes.export_template.hints.tiptap_default_dossier_directory')
.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' } } .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
= f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input' } = f.hidden_field "[dossier_folder][template]", data: { tiptap_target: 'input' }, value: export_template.dossier_folder.template_json
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) = 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' } } = render Dsfr::NoticeComponent.new(data_attributes: { class: 'fr-my-4w' }) do |c|
= f.label :tiptap_pdf_name, class: "fr-label" do - c.with_title do
= f.object.class.human_attribute_name(:tiptap_pdf_name) Sélectionnez les fichiers que vous souhaitez exporter
= render EditableChamp::AsteriskMandatoryComponent.new
%span.fr-hint-text
= t('activerecord.attributes.export_template.hints.tiptap_pdf_name')
.tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } }
= f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input' }
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags })
- if @all_pj.any? %h3 Dossier au format PDF
%h3 Pieces justificatives = render partial: 'export_item',
locals: { item: export_template.export_pdf,
libelle: ExportTemplate.human_attribute_name(:export_pdf),
prefix: 'export_template[export_pdf]' }
.fr-highlight - if procedure.exportables_pieces_jointes_for_all_versions.any?
%p.fr-text--sm %h3 Pièces justificatives
N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes.
- @all_pj.each do |pj| - procedure.exportables_pieces_jointes.each do |tdc|
.fr-input-group{ data: { controller: 'tiptap' } } - item = export_template.pj(tdc)
= label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" = render partial: 'export_item',
.tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } locals: { item:,
= hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input' } libelle: tdc.libelle,
.fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.tags_for_pj }) prefix: 'export_template[pjs][]'}
- 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) }
- 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 .fixed-footer
.fr-container .fr-container
%ul.fr-btns-group.fr-btns-group--inline-md %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 %li
%input.hidden{ type: 'submit', formaction: preview_instructeur_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'get' } = link_to "Supprimer",
= f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' } [:instructeur, procedure, export_template],
%li method: :delete,
= link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary" data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"},
- if @export_template.persisted? class: "fr-btn fr-btn--secondary"
%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 .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 #preview.export-template-preview.fr-p-2w.sticky--top
%h2.fr-h4 Aperçu %h2.fr-h4 Aperçu
- 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 %ul.tree.fr-text--sm
%li #{DownloadableFileService::EXPORT_DIRNAME}/ %li
%span.fr-icon-folder-zip-line
#{DownloadableFileService::EXPORT_DIRNAME}/
%li %li
%ul %ul
%li %li
%span#preview_default_dossier_directory #{export_template.tiptap_convert(dossier, "default_dossier_directory")}/ %span.fr-icon-folder-line
%ul #{export_template.dossier_folder.path(dossier)}/
%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 %ul
- if export_template.export_pdf.enabled?
%li %li
%span messagerie/ %span.fr-icon-pdf-2-line
%ul #{export_template.export_pdf.path(dossier)}.pdf
%li un-autre-fichier.png
- 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 .fr-container
%h1 Mise à jour modèle d'export %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')]] } [t('.title')]] }
.fr-container .fr-container
%h1 Nouveau modèle d'export %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 %tbody
- @export_templates.each do |export_template| - @export_templates.each do |export_template|
%tr %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? %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many?
%p %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 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: export_template:
hints: hints:
name: "The name will be visible by you and the other instructors" 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?" dossier_folder: "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?" export_pdf: "How would you like to name the pdf file containing all the user's answers?"
name: "Template's name" name: "Template's name"
tiptap_default_dossier_directory: "Directory's name for pdf format" dossier_folder: "Directory's name for pdf format"
tiptap_pdf_name: "Export's filename" export_pdf: "File in pdf format"
errors: errors:
models: models:
export_template: 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: export_template:
hints: hints:
name: "Le nom sera visible par vous et les autres instructeurs pour générer un export" 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 ?" dossier_folder: "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 ?" export_pdf: "Comment souhaitez-vous nommer le fichier pdf qui contient toutes les réponses de l'usager ?"
name: "Nom du modèle" name: "Nom du modèle"
tiptap_default_dossier_directory: Nom du répertoire dossier_folder: Nom du répertoire
tiptap_pdf_name: "Nom du dossier au format pdf" export_pdf: "Dossier au format pdf"
errors: errors:
models: models:
export_template: 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 scope module: 'instructeurs', as: 'instructeur' do
resources :procedures, only: [:index, :show], param: :procedure_id do resources :procedures, only: [] do
member do
resources :archives, only: [:index, :create]
resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do
collection do collection do
get 'preview' get 'preview'
end end
end end
end
resources :procedures, only: [:index, :show], param: :procedure_id do
member do
resources :archives, only: [:index, :create]
resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do
resource :contact_information 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| create_table "export_templates", force: :cascade do |t|
t.jsonb "content", default: {} t.jsonb "content", default: {}
t.datetime "created_at", null: false 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.bigint "groupe_instructeur_id", null: false
t.string "kind", null: false t.string "kind", null: false
t.string "name", null: false t.string "name", null: false
t.jsonb "pjs", default: [], null: false, array: true
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["groupe_instructeur_id"], name: "index_export_templates_on_groupe_instructeur_id" t.index ["groupe_instructeur_id"], name: "index_export_templates_on_groupe_instructeur_id"
end end

View file

@ -1,58 +1,31 @@
describe Instructeurs::ExportTemplatesController, type: :controller do describe Instructeurs::ExportTemplatesController, type: :controller do
before { sign_in(instructeur.user) } 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(:instructeur) { create(:instructeur) }
let(:procedure) do let(:procedure) do
create( create(
:procedure, instructeurs: [instructeur], :procedure, instructeurs: [instructeur],
types_de_champ_public: [ types_de_champ_public: [{ type: :piece_justificative, libelle: "pj1", stable_id: 3 }]
{ type: :piece_justificative, libelle: "pj1", stable_id: 3 },
{ type: :piece_justificative, libelle: "pj2", stable_id: 5 },
{ type: :piece_justificative, libelle: "pj3", stable_id: 10 }
]
) )
end end
let(:groupe_instructeur) { procedure.defaut_groupe_instructeur } 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 describe '#new' do
let(:subject) { get :new, params: { procedure_id: procedure.id } } subject { get :new, params: { procedure_id: procedure.id } }
it do it do
subject subject
@ -61,18 +34,22 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
end end
describe '#create' do 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 context 'with valid params' do
it 'redirect to some page' do it 'redirect to some page' do
subject 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éé" expect(flash.notice).to eq "Le modèle d'export coucou a bien été créé"
end end
end end
context 'with invalid params' do 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 it 'display error notification' do
subject subject
expect(flash.alert).to be_present 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 context 'with procedure not accessible by current instructeur' do
let(:another_procedure) { create(:procedure) } 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 it 'raise exception' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound) expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end end
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 end
describe '#edit' do describe '#edit' do
let(:export_template) { create(:export_template, groupe_instructeur:) } 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 it 'render edit' do
subject subject
@ -109,42 +107,53 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
describe '#update' do describe '#update' do
let(:export_template) { create(:export_template, groupe_instructeur:) } let(:export_template) { create(:export_template, groupe_instructeur:) }
let(:tiptap_pdf_name) { let(:export_pdf) { item_params(text: "exPort_") }
{
"type" => "doc",
"content" => [
{ "type" => "paragraph", "content" => [{ "text" => "exPort_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }
]
}.to_json
}
let(:subject) { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } } subject { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } }
context 'with valid params' do context 'with valid params' do
it 'redirect to some page' do it 'redirect to some page' do
subject 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é" 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
end end
context 'with invalid params' do 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 it 'display error notification' do
subject subject
expect(flash.alert).to be_present expect(flash.alert).to be_present
end end
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 end
describe '#destroy' do describe '#destroy' do
let(:export_template) { create(:export_template, groupe_instructeur:) } 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 context 'with valid params' do
it 'redirect to some page' do it 'redirect to some page' do
subject 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é" expect(flash.notice).to eq "Le modèle d'export Mon export a bien été supprimé"
end end
end end
@ -155,7 +164,7 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
let(:export_template) { create(:export_template, groupe_instructeur:) } 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 it '' do
dossier = create(:dossier, procedure: procedure, for_procedure_preview: true) 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" expect(response.body).to include "mon_export_#{dossier.id}.pdf"
end end
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 end

View file

@ -2,86 +2,11 @@ FactoryBot.define do
factory :export_template do factory :export_template do
name { "Mon export" } name { "Mon export" }
groupe_instructeur groupe_instructeur
content { initialize_with { ExportTemplate.default(name:, groupe_instructeur: groupe_instructeur) }
{
"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" }
to_create do |export_template, _context| trait :enabled_pjs do
export_template.set_default_values after(:build) do |export_template, _evaluator|
export_template.save export_template.pjs.each { _1.instance_variable_set('@enabled', true) }
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
end end
end end
end end

View file

@ -1,6 +1,8 @@
describe PiecesJointesListConcern do describe PiecesJointesListConcern do
describe '#pieces_jointes_list' do describe '#pieces_jointes_list' do
include Logic include Logic
describe 'public_wrapped_partionned_pjs and exportables_pieces_jointes' do
let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) } let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) }
let(:types_de_champ_public) do let(:types_de_champ_public) do
[ [
@ -46,5 +48,31 @@ describe PiecesJointesListConcern do
it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do 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)) expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle))
end 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
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 end
context 'with export template' do 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 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) .to change { Export.count }.by(1)
end end
end end

View file

@ -1,8 +1,7 @@
describe ExportTemplate do describe ExportTemplate do
let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) } let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) }
let(:export_template) { create(:export_template, :with_custom_content, groupe_instructeur:, content:) } let(:export_template) { build(:export_template, groupe_instructeur:) }
let(:procedure) { create(:procedure_with_dossiers, types_de_champ_public:, for_individual:) } let(:procedure) { create(:procedure, types_de_champ_public:, for_individual:) }
let(:dossier) { procedure.dossiers.first }
let(:for_individual) { false } let(:for_individual) { false }
let(:types_de_champ_public) do let(:types_de_champ_public) do
[ [
@ -10,335 +9,80 @@ describe ExportTemplate do
{ type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 5 } { type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 5 }
] ]
end 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 describe '.default' do
let(:export_template) { build(:export_template, groupe_instructeur: groupe_instructeur) }
it 'set default values' do it 'set default values' do
export_template.set_default_values expect(export_template.export_pdf).to eq(ExportItem.default(prefix: "export", enabled: true))
expect(export_template.content).to eq({ expect(export_template.dossier_folder).to eq(ExportItem.default(prefix: "dossier", enabled: true))
"pdf_name" => { expect(export_template.pjs).to eq([ExportItem.default(stable_id: 3, prefix: "justificatif-de-domicile", enabled: false)])
"type" => "doc",
"content" => [
{ "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }
]
},
"default_dossier_directory" => {
"type" => "doc",
"content" => [
{ "type" => "paragraph", "content" => [{ "text" => "dossier-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }
]
},
"pjs" =>
[
{
"stable_id" => "3",
"path" => { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "justificatif-de-domicile-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }] }
}
]
})
end end
end end
describe '#assign_pj_names' do describe '#pj' do
let(:pj_params) do context 'when pj exists' do
{ subject { export_template.pj(double(stable_id: 3)) }
"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" } }] }] it { is_expected.to eq(ExportItem.default(stable_id: 3, prefix: "justificatif-de-domicile", enabled: false)) }
}.to_json
}
end end
it 'values content from pj params' do
export_template.assign_pj_names(pj_params) context 'when pj does not exist' do
expect(export_template.content["pjs"]).to eq [ subject { export_template.pj(TypeDeChamp.new(libelle: 'hi', stable_id: 10)) }
{ :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" }
] it { is_expected.to eq(ExportItem.default(stable_id: 10, prefix: "hi", enabled: false)) }
end end
end end
describe '#tiptap_default_dossier_directory' do describe '#attachment_path' do
it 'returns tiptap_default_dossier_directory from content' do let(:dossier) { create(:dossier, :en_construction, procedure:) }
expect(export_template.tiptap_default_dossier_directory).to eq({
"type" => "doc",
"content" => [
{ "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }
]
}.to_json)
end
end
describe '#tiptap_pdf_name' do
it 'returns tiptap_pdf_name from content' do
expect(export_template.tiptap_pdf_name).to eq({
"type" => "doc",
"content" => [
{ "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }
]
}.to_json)
end
end
describe '#content_for_pj' do
let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) }
let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) }
let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) }
it 'returns tiptap content for pj' do
expect(export_template.content_for_pj(type_de_champ_pj)).to eq({
"type" => "doc",
"content" => [
{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "original-filename", "label" => "nom original du fichier" } }, { "text" => " _justif", "type" => "text" }] }
]
}.to_json)
end
end
describe '#attachment_and_path' do
let(:dossier) { create(:dossier) }
context 'for export pdf' do 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 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_path(dossier, attachment)).to eq("DOSSIER-#{dossier.id}/mon_export-#{dossier.id}.pdf")
expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/mon_export_#{dossier.id}.pdf"])
end end
end end
context 'for pj' do context 'for pj' do
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) } let(:champ_pj) { dossier.champs_public.first }
let(:champ_pj) { dossier.champs.find(&:piece_justificative?) } 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")) } 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 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"]) expect(export_template.attachment_path(dossier, attachment, champ: champ_pj)).to eq("dossier-#{dossier.id}/justif-#{dossier.id}-01.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"])
end end
end end
end end
describe '#tiptap_convert' do describe '#tags and #pj_tags' do
it 'convert default dossier directory' do let(:procedure) { build(:procedure, for_individual:) }
expect(export_template.tiptap_convert(procedure.dossiers.first, "default_dossier_directory")).to eq "DOSSIER_#{dossier.id}"
end
it 'convert pdf_name' do
expect(export_template.tiptap_convert(procedure.dossiers.first, "pdf_name")).to eq "mon_export_#{dossier.id}"
end
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
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 context 'for entreprise procedure' do
let(:for_individual) { false } let(:for_individual) { false }
describe 'specific_tags' do let(:expected_tags) do
it 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']
tags = export_template.specific_tags
expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"]
end
end end
describe 'tags_for_pj' do
it do it do
tags = export_template.tags_for_pj expect(export_template.tags.map { _1[:id] }).to eq(expected_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", "original-filename"] expect(export_template.pj_tags.map { _1[:id] }).to eq(expected_tags + ['original-filename'])
end
end end
end end
context 'for individual procedure' do context 'for individual procedure' do
let(:for_individual) { true } let(:for_individual) { true }
describe 'specific_tags' do let(:expected_tags) do
it do ['individual_gender', 'individual_last_name', 'individual_first_name', 'dossier_depose_at', 'dossier_procedure_libelle', 'dossier_service_name', 'dossier_number', 'dossier_groupe_instructeur']
tags = export_template.specific_tags
expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"]
end
end end
describe 'tags_for_pj' do
it do it do
tags = export_template.tags_for_pj expect(export_template.tags.map { _1[:id] }).to eq(expected_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", "original-filename"] expect(export_template.pj_tags.map { _1[:id] }).to eq(expected_tags + ['original-filename'])
end end
end 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, :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") } it { expect(create(:type_de_champ, libelle: " fix me ").libelle).to eq("fix me") }
end 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 end

View file

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

View file

@ -450,7 +450,7 @@ describe ProcedureExportService do
context 'with export_template' do context 'with export_template' do
let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) }
let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) } 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 before do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
end end
@ -465,10 +465,9 @@ describe ProcedureExportService do
structure = [ structure = [
"#{base_fn}/", "#{base_fn}/",
"#{base_fn}/dossier-#{dossier.id}/", "#{base_fn}/dossier-#{dossier.id}/",
"#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-1.txt", "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-01.txt",
"#{base_fn}/dossier-#{dossier.id}/export_#{dossier.id}.pdf" "#{base_fn}/dossier-#{dossier.id}/export-#{dossier.id}.pdf"
] ]
expect(files.size).to eq(structure.size)
expect(files.map(&:filename)).to match_array(structure) expect(files.map(&:filename)).to match_array(structure)
end end
FileUtils.remove_entry_secure('tmp.zip') FileUtils.remove_entry_secure('tmp.zip')

View file

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

View file

@ -193,8 +193,10 @@ RSpec.describe TiptapService do
end end
end end
describe '.to_path' do describe '.to_texts_and_tags' do
let(:substitutions) { { "dossier_number" => "42" } } subject { described_class.new.to_texts_and_tags(json, substitutions) }
context 'nominal' do
let(:json) do let(:json) do
{ {
"content" => [ "content" => [
@ -204,8 +206,27 @@ RSpec.describe TiptapService do
}.deep_symbolize_keys }.deep_symbolize_keys
end end
it 'returns path' do context 'with substitutions' do
expect(described_class.new.to_path(json, substitutions)).to eq("export_42.pdf") 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
context 'empty paragraph' do
let(:json) { { content: [{ type: 'paragraph' }] } }
let(:substitutions) { {} }
it { is_expected.to eq('') }
end end
end 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