From 93ad0f4bda3ab72b0ef5c8e8b334b376e0753275 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sat, 2 Mar 2024 22:41:05 +0100 Subject: [PATCH 01/59] [tiptap] convert tiptap json to path --- app/services/tiptap_service.rb | 21 +++++++++++++++++++++ spec/services/tiptap_service_spec.rb | 16 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/app/services/tiptap_service.rb b/app/services/tiptap_service.rb index ce372d39f..5d0cb4325 100644 --- a/app/services/tiptap_service.rb +++ b/app/services/tiptap_service.rb @@ -5,6 +5,12 @@ class TiptapService children(node[:content], substitutions, 0) end + def to_path(node, substitutions = {}) + return '' if node.nil? + + children_path(node[:content], substitutions) + end + # NOTE: node must be deep symbolized keys def used_tags_and_libelle_for(node, tags = Set.new) case node @@ -25,6 +31,21 @@ class TiptapService @body_started = false end + def children_path(content, substitutions) + content.map { node_to_path(_1, substitutions) }.join + end + + def node_to_path(node, substitutions) + case node + in type: 'paragraph', content: + children_path(content, substitutions) + in type: 'text', text:, **rest + text.strip + in type: 'mention', attrs: { id: }, **rest + substitutions.fetch(id) { "--#{id}--" } + end + end + def children(content, substitutions, level) content.map { node_to_html(_1, substitutions, level) }.join end diff --git a/spec/services/tiptap_service_spec.rb b/spec/services/tiptap_service_spec.rb index 14196fb53..da47220f2 100644 --- a/spec/services/tiptap_service_spec.rb +++ b/spec/services/tiptap_service_spec.rb @@ -192,4 +192,20 @@ RSpec.describe TiptapService do expect(described_class.new.used_tags_and_libelle_for(json)).to eq(Set.new([['name', 'Nom']])) end end + + describe '.to_path' do + let(:substitutions) { { "dossier_number" => "42" } } + let(:json) do + { + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " .pdf", "type" => "text" }] } + ] + + }.deep_symbolize_keys + end + + it 'returns path' do + expect(described_class.new.to_path(json, substitutions)).to eq("export_42.pdf") + end + end end From 474eb18016d50a41e6289fa26402d5a83dd9deda Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sat, 2 Mar 2024 21:47:48 +0100 Subject: [PATCH 02/59] add export template migration --- db/migrate/20240130154452_create_export_templates.rb | 12 ++++++++++++ db/schema.rb | 11 +++++++++++ 2 files changed, 23 insertions(+) create mode 100644 db/migrate/20240130154452_create_export_templates.rb diff --git a/db/migrate/20240130154452_create_export_templates.rb b/db/migrate/20240130154452_create_export_templates.rb new file mode 100644 index 000000000..f1306af2f --- /dev/null +++ b/db/migrate/20240130154452_create_export_templates.rb @@ -0,0 +1,12 @@ +class CreateExportTemplates < ActiveRecord::Migration[7.0] + def change + create_table :export_templates do |t| + t.string :name, null: false + t.string :kind, null: false + t.jsonb :content, default: {} + t.belongs_to :groupe_instructeur, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b936bf6b3..054c32caa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -593,6 +593,16 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do t.index ["procedure_id"], name: "index_experts_procedures_on_procedure_id" end + create_table "export_templates", force: :cascade do |t| + t.jsonb "content", default: {} + t.datetime "created_at", null: false + t.bigint "groupe_instructeur_id", null: false + t.string "kind", null: false + t.string "name", null: false + t.datetime "updated_at", null: false + t.index ["groupe_instructeur_id"], name: "index_export_templates_on_groupe_instructeur_id" + end + create_table "exports", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.integer "dossiers_count" @@ -1224,6 +1234,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do add_foreign_key "experts", "users" add_foreign_key "experts_procedures", "experts" add_foreign_key "experts_procedures", "procedures" + add_foreign_key "export_templates", "groupe_instructeurs" add_foreign_key "exports", "instructeurs" add_foreign_key "france_connect_informations", "users" add_foreign_key "geo_areas", "champs" From d1c3b84ea217bc524faf4b7b5d5ef172360176ee Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sat, 2 Mar 2024 22:13:09 +0100 Subject: [PATCH 03/59] add export template model --- app/models/export_template.rb | 125 ++++++++++++++++++++++++++++ app/models/groupe_instructeur.rb | 1 + app/models/instructeur.rb | 1 + app/models/procedure.rb | 1 + spec/factories/export_template.rb | 34 ++++++++ spec/models/export_template_spec.rb | 76 +++++++++++++++++ 6 files changed, 238 insertions(+) create mode 100644 app/models/export_template.rb create mode 100644 spec/factories/export_template.rb create mode 100644 spec/models/export_template_spec.rb diff --git a/app/models/export_template.rb b/app/models/export_template.rb new file mode 100644 index 000000000..30897d081 --- /dev/null +++ b/app/models/export_template.rb @@ -0,0 +1,125 @@ +class ExportTemplate < ApplicationRecord + include TagsSubstitutionConcern + + belongs_to :groupe_instructeur + has_one :procedure, through: :groupe_instructeur + + DOSSIER_STATE = Dossier.states.fetch(:en_construction) + + def tiptap_default_dossier_directory=(body) + self.content["default_dossier_directory"] = JSON.parse(body) + end + + def tiptap_default_dossier_directory + tiptap_content("default_dossier_directory") + end + + def tiptap_pdf_name=(body) + self.content["pdf_name"] = JSON.parse(body) + end + + def tiptap_pdf_name + tiptap_content("pdf_name") + end + + def attachment_and_path(dossier, attachment, index: 0, row_index: nil) + [ + attachment, + path(dossier, attachment, index, row_index) + ] + 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) + if content_for_pj_id(pj_stable_id)["content"]&.first["content"] + render_attributes_for(content_for_pj_id(pj_stable_id), dossier) + end + end + + def render_attributes_for(content_for, dossier) + tiptap = TiptapService.new + used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys) + substitutions = tags_substitutions(used_tags, dossier, escape: false) + tiptap.to_path(content_for.deep_symbolize_keys, substitutions) + 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 + + private + + def tiptap_content(key) + content[key]&.to_json + end + + def tiptap_json(prefix) + { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => prefix, "type" => "text" }, { "type" => "mention", "attrs" => DOSSIER_ID_TAG.stringify_keys }] } + ] + } + end + + def content_for_pj_id(stable_id) + content_for_stable_id = content["pjs"].find { _1.symbolize_keys[:stable_id] == stable_id.to_s } + content_for_stable_id.symbolize_keys.fetch(:path) + end + + + def path(dossier, attachment, index, row_index) + if attachment.name == 'pdf_export_for_instructeur' + return export_path(dossier) + end + + dir_path = case attachment.record_type + when 'Dossier' + 'dossier' + when 'Commentaire' + 'messagerie' + when 'Avis' + 'avis' + else + # for attachment + return attachment_path(dossier, attachment, index, row_index) + end + + File.join(folder(dossier), dir_path, attachment.filename.to_s) + end + + def attachment_path(dossier, attachment, index, row_index) + type_de_champ_id = dossier.champs.find(attachment.record_id).type_de_champ_id + stable_id = TypeDeChamp.find(type_de_champ_id).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) + 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 = "" + if index >= 1 && !row_index.nil? + suffix += "-#{index + 1}" + suffix += "-#{row_index + 1}" if row_index + end + + suffix + attachment.filename.extension_with_delimiter + end +end diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index b0b165b1e..bd9aad659 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -9,6 +9,7 @@ class GroupeInstructeur < ApplicationRecord has_many :batch_operations, through: :dossiers, source: :batch_operations has_many :assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :groupe_instructeur has_many :previous_assignments, class_name: 'DossierAssignment', dependent: :nullify, inverse_of: :previous_groupe_instructeur + has_many :export_templates has_and_belongs_to_many :exports, dependent: :destroy has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index 707e4da98..0b724d337 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -14,6 +14,7 @@ class Instructeur < ApplicationRecord has_many :batch_operations, dependent: :nullify has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo', inverse_of: :instructeur has_many :groupe_instructeur_with_email_notifications, through: :assign_to_with_email_notifications, source: :groupe_instructeur + has_many :export_templates, through: :groupe_instructeurs has_many :commentaires, inverse_of: :instructeur, dependent: :nullify has_many :dossiers, -> { state_not_brouillon }, through: :unordered_groupe_instructeurs diff --git a/app/models/procedure.rb b/app/models/procedure.rb index fe7005599..aa0c5695c 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -153,6 +153,7 @@ class Procedure < ApplicationRecord has_many :administrateurs, through: :administrateurs_procedures, after_remove: -> (procedure, _admin) { procedure.validate! } has_many :groupe_instructeurs, -> { order(:label) }, inverse_of: :procedure, dependent: :destroy has_many :instructeurs, through: :groupe_instructeurs + has_many :export_templates, through: :groupe_instructeurs has_many :active_groupe_instructeurs, -> { active }, class_name: 'GroupeInstructeur', inverse_of: false has_many :closed_groupe_instructeurs, -> { closed }, class_name: 'GroupeInstructeur', inverse_of: false diff --git a/spec/factories/export_template.rb b/spec/factories/export_template.rb new file mode 100644 index 000000000..0f4e8d882 --- /dev/null +++ b/spec/factories/export_template.rb @@ -0,0 +1,34 @@ +FactoryBot.define do + factory :export_template do + name { "Mon export" } + groupe_instructeur + content { + { + "pdf_name" => + { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_id", "label" => "id dossier" } }, { "text" => " .pdf", "type" => "text" }] } + ] + }, + "default_dossier_directory" => + { + "type" => "doc", + "content" => + [ + { + "type" => "paragraph", + "content" => + [ + { "text" => "dossier_", "type" => "text" }, + { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, + { "text" => " ", "type" => "text" } + ] + } + ] + } + } +} + kind { "zip" } + end +end diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb new file mode 100644 index 000000000..32a3330d5 --- /dev/null +++ b/spec/models/export_template_spec.rb @@ -0,0 +1,76 @@ +describe ExportTemplate do + let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) } + let(:export_template) { build(:export_template, groupe_instructeur:, content:) } + let(:procedure) { create(:procedure_with_dossiers) } + 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"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" _justif", "type"=>"text"}]}]}, stable_id: "3"}, + { path: + {"type"=>"doc", "content"=>[{"type"=>"paragraph", "content"=>[{"text"=>"cni_", "type"=>"text"}, {"type"=>"mention", "attrs"=>{"id"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" ", "type"=>"text"}]}]}, + stable_id: "5"}, + { path: {"type"=>"doc", "content"=>[{"type"=>"paragraph", "content"=>[{"text"=>"pj_repet_", "type"=>"text"}, {"type"=>"mention", "attrs"=>{"id"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" ", "type"=>"text"}]}]}, + stable_id: "10"} + ] + } + end + + describe 'new' do + let(:export_template) { build(:export_template, groupe_instructeur: groupe_instructeur) } + let(:procedure) { create(:procedure, types_de_champ_public:) } + let(:types_de_champ_public) do + [ + { type: :integer_number, stable_id: 900 }, + { type: :piece_justificative, libelle: "Justificatif de domicile", mandatory: true, stable_id: 910 } + ] + end + end + + describe '#tiptap_default_dossier_directory' do + it 'returns tiptap_default_dossier_directory from content' do + expect(export_template.tiptap_default_dossier_directory).to eq({ + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] } + ] + }.to_json) + end + end + + describe '#tiptap_pdf_name' do + it 'returns tiptap_pdf_name from content' do + expect(export_template.tiptap_pdf_name).to eq({ + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } + ] + }.to_json) + end + end + + describe '#attachment_and_path' do + let(:dossier) { create(:dossier) } + + context 'for export pdf' do + let(:attachment) { double("attachment") } + + it 'gives absolute filename for export of specific dossier' do + allow(attachment).to receive(:name).and_return('pdf_export_for_instructeur') + expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/mon_export_#{dossier.id}.pdf"]) + end + end + end +end From 24922605cdfaff2a43dc7dad15df42d2110d5b6d Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 10:03:12 +0100 Subject: [PATCH 04/59] add pj for export template --- app/models/export_template.rb | 9 +++++ spec/models/export_template_spec.rb | 62 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 30897d081..eaa753415 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -22,6 +22,15 @@ class ExportTemplate < ApplicationRecord tiptap_content("pdf_name") end + def content_for_pj(pj) + content_for_pj_id(pj.stable_id)&.to_json + 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 attachment_and_path(dossier, attachment, index: 0, row_index: nil) [ attachment, diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index 32a3330d5..9a2a29aaf 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -61,6 +61,22 @@ describe ExportTemplate do 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"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" _justif", "type"=>"text"}]} + ] + }.to_json) + end + end + describe '#attachment_and_path' do let(:dossier) { create(:dossier) } @@ -72,5 +88,51 @@ describe ExportTemplate do expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/mon_export_#{dossier.id}.pdf"]) end end + + context 'for pj' do + let(:dossier) { procedure.dossiers.first } + let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, procedure:) } + let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) } + + let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } + + before do + dossier.champs_public << champ_pj + end + it 'returns pj and custom name for pj' do + expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/#{dossier.id}_justif.png"]) + end + end + context 'pj repetable' do + let(:procedure) do + create(:procedure_with_dossiers, :for_individual, types_de_champ_public: [{ type: :repetition, mandatory: true, children: [{ libelle: 'sub type de champ' }] }]) + end + let(:type_de_champ_repetition) do + repetition = draft.types_de_champ_public.repetition.first + repetition.update(stable_id: 3333) + repetition + end + let(:draft) { procedure.draft_revision } + let(:dossier) { procedure.dossiers.first } + + let(:type_de_champ_pj) do + draft.add_type_de_champ({ + type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative), + libelle: "pj repet", + stable_id: 10, + parent_stable_id: type_de_champ_repetition.stable_id + }) + end + let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) } + + let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } + + before do + dossier.champs_public << champ_pj + end + it 'rename repetable pj' do + expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/pj_repet_#{dossier.id}.png"]) + end + end end end From 25ab2420fec0a69d4f6424021df7a81fe9ef0af0 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 10:11:26 +0100 Subject: [PATCH 05/59] validate export template --- app/models/export_template.rb | 1 + app/validators/export_template_validator.rb | 54 +++++++++ spec/models/export_template_spec.rb | 119 ++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 app/validators/export_template_validator.rb diff --git a/app/models/export_template.rb b/app/models/export_template.rb index eaa753415..5345116bb 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -3,6 +3,7 @@ class ExportTemplate < ApplicationRecord belongs_to :groupe_instructeur has_one :procedure, through: :groupe_instructeur + validates_with ExportTemplateValidator DOSSIER_STATE = Dossier.states.fetch(:en_construction) diff --git a/app/validators/export_template_validator.rb b/app/validators/export_template_validator.rb new file mode 100644 index 000000000..1f3475040 --- /dev/null +++ b/app/validators/export_template_validator.rb @@ -0,0 +1,54 @@ +class ExportTemplateValidator < ActiveModel::Validator + def validate(record) + validate_default_dossier_directory(record) + validate_pdf_name(record) + validate_pjs(record) + end + + private + + def validate_default_dossier_directory(record) + mention = attribute_content_mention(record, :default_dossier_directory) + if mention&.fetch("id", nil) != "dossier_number" + record.errors.add :tiptap_default_dossier_directory, :dossier_number_mandatory + end + end + + def validate_pdf_name(record) + if attribute_content_text(record, :pdf_name).blank? && attribute_content_mention(record, :pdf_name).blank? + record.errors.add :tiptap_pdf_name, :blank + end + end + + def attribute_content_text(record, attribute) + attribute_content(record, attribute)&.find { |elem| elem["type"] == "text" }&.fetch("text", nil) + end + + def attribute_content_mention(record, attribute) + attribute_content(record, attribute)&.find { |elem| elem["type"] == "mention" }&.fetch("attrs", nil) + end + + def attribute_content(record, attribute) + content = record.content[attribute.to_s]&.fetch("content", nil) + if content.is_a?(Array) + content.first&.fetch("content", nil) + end + end + + def validate_pjs(record) + record.content["pjs"]&.each do |pj| + pj_sym = pj.symbolize_keys + libelle = record.groupe_instructeur.procedure.pieces_jointes_exportables_list.find { _1.stable_id.to_s == pj_sym[:stable_id] }&.libelle&.to_sym + validate_content(record, pj_sym[:path], libelle) + end + end + + def validate_content(record, attribute_content, attribute) + if attribute_content.nil? || attribute_content["content"].nil? || + attribute_content["content"].first.nil? || + attribute_content["content"].first["content"].nil? || + (attribute_content["content"].first["content"].find { |elem| elem["text"].blank? } && attribute_content["content"].first["content"].find { |elem| elem["type"] == "mention" }["attrs"].blank?) + record.errors.add attribute, I18n.t(:blank, scope: 'errors.messages') + end + end +end diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index 9a2a29aaf..d8df04760 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -135,4 +135,123 @@ describe ExportTemplate do end end end + + describe '#valid?' do + let(:subject) { build(:export_template, 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 + expect(subject.errors[:default_dossier_directory]).not_to be_present + 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 + expect(subject.errors[:default_dossier_directory]).not_to be_present + end + end + + context 'without mention' do + let(:ddd_mention) { { "type" => "mention", "attrs" => {} } } + it "add error for default_dossier_directory" do + expect(subject.valid?).to be_falsey + expect(subject.errors[:default_dossier_directory]).to be_present + end + end + + context 'with mention but without numéro de dossier' do + let(:ddd_mention) { { "type" => "mention", "attrs" => { "id" => 'dossier_service_name', "label" => "nom du service" } } } + it "add error for default_dossier_directory" do + expect(subject.valid?).to be_falsey + expect(subject.errors[:default_dossier_directory]).to be_present + 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[: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[:default_dossier_directory]).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[:pdf_name]).to be_present + end + end + end + + context 'with no pj text' do + let(:pj_text) { " " } + + context 'with mention' do + it 'has no error for pj' do + expect(subject.valid?).to be_truthy + expect(subject.errors[:pj_3]).not_to be_present + 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[:pj_3]).to be_present + end + end + end + end end From bbb6309b4f67f81cdff5ea2c4a92a091434965aa Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 10:27:48 +0100 Subject: [PATCH 06/59] procedure: add pieces_jointes_exportables_list --- app/models/procedure.rb | 26 ++++++++++++++++++++++---- app/models/procedure_revision.rb | 1 + spec/models/procedure_spec.rb | 26 +++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index aa0c5695c..02b1a3d15 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -992,6 +992,12 @@ class Procedure < ApplicationRecord end end + def pieces_jointes_exportables_list + pieces_jointes_list(with_private: true, with_titre_identite: false, with_repetition_parent: false) do |base_scope| + base_scope + end.flatten + end + def pieces_jointes_list_with_conditionnal pieces_jointes_list do |base_scope| base_scope.where.not(types_de_champ: { condition: nil }) @@ -1025,15 +1031,27 @@ class Procedure < ApplicationRecord private - def pieces_jointes_list - scope = yield active_revision.revision_types_de_champ_public + def pieces_jointes_list(with_private: false, with_titre_identite: true, with_repetition_parent: true) + types_de_champ = with_private ? + active_revision.revision_types_de_champ_private_and_public : + active_revision.revision_types_de_champ_public + + type_champs = ['repetition', 'piece_justificative'] + type_champs << 'titre_identite' if with_titre_identite + + scope = yield types_de_champ .includes(:type_de_champ, revision_types_de_champ: :type_de_champ) - .where(types_de_champ: { type_champ: ['repetition', 'piece_justificative', 'titre_identite'] }) + .where(types_de_champ: { type_champ: [type_champs] }) scope.each_with_object([]) do |rtdc, list| if rtdc.type_de_champ.repetition? rtdc.revision_types_de_champ.each do |rtdc_in_repetition| - list << [rtdc_in_repetition.type_de_champ, rtdc.type_de_champ] if rtdc_in_repetition.type_de_champ.piece_justificative? + if rtdc_in_repetition.type_de_champ.piece_justificative? + to_add = [] + to_add << rtdc_in_repetition.type_de_champ + to_add << rtdc.type_de_champ if with_repetition_parent + list << to_add + end end else list << [rtdc.type_de_champ] diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index c8daa1bec..8326c6533 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -8,6 +8,7 @@ class ProcedureRevision < ApplicationRecord has_many :revision_types_de_champ, -> { order(:position, :id) }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_public, -> { root.public_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_private, -> { root.private_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision + has_many :revision_types_de_champ_private_and_public, -> { root.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :types_de_champ, through: :revision_types_de_champ, source: :type_de_champ has_many :types_de_champ_public, through: :revision_types_de_champ_public, source: :type_de_champ has_many :types_de_champ_private, through: :revision_types_de_champ_private, source: :type_de_champ diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 01f937995..8b036cddc 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -1748,13 +1748,23 @@ describe Procedure do describe '#pieces_jointes_list' do include Logic - let(:procedure) { create(:procedure, types_de_champ_public:) } + let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) } let(:types_de_champ_public) do [ { type: :integer_number, stable_id: 900 }, { type: :piece_justificative, libelle: "PJ", mandatory: true, stable_id: 910 }, { type: :piece_justificative, libelle: "PJ-cond", mandatory: true, stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) }, - { type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "PJ2", stable_id: 921 }] } + { type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "PJ2", stable_id: 921 }] }, + { type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 930 } + ] + end + + let(:types_de_champ_private) do + [ + { type: :integer_number, stable_id: 950 }, + { type: :piece_justificative, libelle: "PJ", mandatory: true, stable_id: 960 }, + { type: :piece_justificative, libelle: "PJ-cond", mandatory: true, stable_id: 961, condition: ds_eq(champ_value(900), constant(1)) }, + { type: :repetition, libelle: "Répétition", stable_id: 970, children: [{ type: :piece_justificative, libelle: "PJ2", stable_id: 971 }] } ] end @@ -1762,14 +1772,24 @@ describe Procedure do let(:pjcond) { procedure.active_revision.types_de_champ.find { _1.stable_id == 911 } } let(:repetition) { procedure.active_revision.types_de_champ.find { _1.stable_id == 920 } } let(:pj2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 921 } } + let(:pj3) { procedure.active_revision.types_de_champ.find { _1.stable_id == 930 } } + + let(:pj5) { procedure.active_revision.types_de_champ.find { _1.stable_id == 960 } } + let(:pjcond2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 961 } } + let(:repetition2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 970 } } + let(:pj6) { procedure.active_revision.types_de_champ.find { _1.stable_id == 971 } } it "returns the list of pieces jointes without conditional" do - expect(procedure.pieces_jointes_list_without_conditionnal).to match_array([[pj1], [pj2, repetition]]) + expect(procedure.pieces_jointes_list_without_conditionnal).to match_array([[pj1], [pj2, repetition], [pj3]]) end it "returns the list of pieces jointes having conditional" do expect(procedure.pieces_jointes_list_with_conditionnal).to match_array([[pjcond]]) end + + it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do + expect(procedure.pieces_jointes_exportables_list).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6]) + end end describe "#attestation_template" do From dbf46b1f029b56964a491cb104ea71540efac612 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 10:22:58 +0100 Subject: [PATCH 07/59] extract DOSSIER_ID_TAG --- .../concerns/tags_substitution_concern.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 8bdc51705..8b0e50b1c 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -61,6 +61,15 @@ module TagsSubstitutionConcern end end + DOSSIER_ID_TAG = { + id: 'dossier_number', + label: 'numéro du dossier', + libelle: 'numéro du dossier', + description: '', + target: :id, + available_for_states: Dossier::SOUMIS + } + DOSSIER_TAGS = [ { id: 'dossier_motivation', @@ -98,13 +107,6 @@ module TagsSubstitutionConcern lambda: -> (d) { d.procedure.libelle }, available_for_states: Dossier::SOUMIS }, - { - id: 'dossier_number', - libelle: 'numéro du dossier', - description: '', - target: :id, - available_for_states: Dossier::SOUMIS - }, { id: 'dossier_service_name', libelle: 'nom du service', @@ -112,7 +114,7 @@ module TagsSubstitutionConcern lambda: -> (d) { d.procedure.organisation_name || '' }, available_for_states: Dossier::SOUMIS } - ] + ].push(DOSSIER_ID_TAG) DOSSIER_TAGS_FOR_MAIL = [ { From a248eba6415112db1e51a3c4b37f901be41f00a7 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 10:19:02 +0100 Subject: [PATCH 08/59] export template: set default values --- app/models/export_template.rb | 10 ++++++++++ spec/models/export_template_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 5345116bb..7016edac7 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -7,6 +7,16 @@ class ExportTemplate < ApplicationRecord DOSSIER_STATE = Dossier.states.fetch(:en_construction) + def set_default_values + content["default_dossier_directory"] = tiptap_json("dossier-") + content["pdf_name"] = tiptap_json("export_") + + content["pjs"] = [] + procedure.pieces_jointes_exportables_list.each do |pj| + content["pjs"] << { "stable_id" => pj.stable_id.to_s, "path" => tiptap_json("#{pj.libelle.parameterize}-") } + end + end + def tiptap_default_dossier_directory=(body) self.content["default_dossier_directory"] = JSON.parse(body) end diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index d8df04760..2aa74744d 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -37,6 +37,31 @@ describe ExportTemplate do { type: :piece_justificative, libelle: "Justificatif de domicile", mandatory: true, stable_id: 910 } ] end + it 'set default values' do + export_template.set_default_values + expect(export_template.content).to eq({ + "pdf_name" => { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] } + ] + }, + "default_dossier_directory" => { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "dossier-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] } + ] + }, + "pjs" => + [ + + { + "stable_id" => "910", + "path" => { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "justificatif-de-domicile-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }] } + } + ] + }) + end end describe '#tiptap_default_dossier_directory' do From 95c308dc51796e5d08110373e584689048a42a85 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 11:13:07 +0100 Subject: [PATCH 09/59] add specific tags --- app/models/export_template.rb | 4 ++++ .../concerns/tags_substitution_concern_spec.rb | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 7016edac7..667b82b7e 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -81,6 +81,10 @@ class ExportTemplate < ApplicationRecord "#{render_attributes_for(content["pdf_name"], dossier)}.pdf" end + def specific_tags + tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten + end + private def tiptap_content(key) diff --git a/spec/models/concerns/tags_substitution_concern_spec.rb b/spec/models/concerns/tags_substitution_concern_spec.rb index e50ecfd70..a11f991f7 100644 --- a/spec/models/concerns/tags_substitution_concern_spec.rb +++ b/spec/models/concerns/tags_substitution_concern_spec.rb @@ -604,6 +604,24 @@ describe TagsSubstitutionConcern, type: :model do end end + describe 'some_tags' do + context 'for entreprise procedure' do + let(:for_individual) { false } + it do + tags = template_concern.some_tags + expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_motivation", "dossier_depose_at", "dossier_en_instruction_at", "dossier_processed_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number"] + end + end + + context 'for individual procedure' do + let(:for_individual) { true } + it do + tags = template_concern.some_tags + expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_motivation", "dossier_depose_at", "dossier_en_instruction_at", "dossier_processed_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number"] + end + end + end + describe 'parser' do it do tokens = TagsSubstitutionConcern::TagsParser.parse("hello world --public--, --numéro du dossier--, un test--yolo-- encore du text\n---\n encore du text --- et encore du text\n--tag--") From f5813b4e55c6d946ff393b80ad0ef8951e60c63d Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 11:30:41 +0100 Subject: [PATCH 10/59] create and update export templates --- .../export_templates_controller.rb | 82 ++++++++++++ app/models/export_template.rb | 8 +- .../export_templates/_form.html.haml | 59 +++++++++ .../export_templates/edit.html.haml | 7 + .../export_templates/new.html.haml | 6 + .../procedures/export_templates/fr.yml | 8 ++ config/routes.rb | 1 + .../export_templates_controller_spec.rb | 120 ++++++++++++++++++ 8 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 app/controllers/instructeurs/export_templates_controller.rb create mode 100644 app/views/instructeurs/export_templates/_form.html.haml create mode 100644 app/views/instructeurs/export_templates/edit.html.haml create mode 100644 app/views/instructeurs/export_templates/new.html.haml create mode 100644 config/locales/views/instructeurs/procedures/export_templates/fr.yml create mode 100644 spec/controllers/instructeurs/export_templates_controller_spec.rb diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb new file mode 100644 index 000000000..4b12a848b --- /dev/null +++ b/app/controllers/instructeurs/export_templates_controller.rb @@ -0,0 +1,82 @@ +module Instructeurs + class ExportTemplatesController < InstructeurController + before_action :set_procedure + before_action :set_groupe_instructeur, only: [:create, :update] + before_action :set_export_template, only: [:edit, :update, :destroy] + before_action :set_groupe_instructeurs + before_action :set_all_pj + + def new + @export_template = ExportTemplate.new(kind: 'zip', groupe_instructeur: @groupe_instructeurs.first) + @export_template.set_default_values + end + + def create + @export_template = @groupe_instructeur.export_templates.build(export_template_params) + @export_template.assign_pj_names(pj_params) + if @export_template.save + redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été créé" + else + flash[:alert] = @export_template.errors.full_messages + render :new + end + end + + def edit + end + + def update + @export_template.assign_attributes(export_template_params) + @export_template.groupe_instructeur = @groupe_instructeur + @export_template.assign_pj_names(pj_params) + if @export_template.save + redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été modifié" + else + flash[:alert] = @export_template.errors.full_messages + render :edit + end + end + + private + + def export_template_params + params.require(:export_template).permit(*export_params) + end + + def set_procedure + @procedure = current_instructeur.procedures.find params[:procedure_id] + Sentry.configure_scope do |scope| + scope.set_tags(procedure: @procedure.id) + end + end + + def set_export_template + @export_template = current_instructeur.export_templates.find(params[:id]) + end + + def set_groupe_instructeur + @groupe_instructeur = @procedure.groupe_instructeurs.find(params.require(:export_template)[:groupe_instructeur_id]) + end + + def set_groupe_instructeurs + @groupe_instructeurs = current_instructeur.groupe_instructeurs.where(procedure: @procedure) + end + + def set_all_pj + @all_pj ||= @procedure.pieces_jointes_exportables_list + 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 diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 667b82b7e..2a7282fdc 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -37,9 +37,11 @@ class ExportTemplate < ApplicationRecord content_for_pj_id(pj.stable_id)&.to_json 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) + 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) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml new file mode 100644 index 000000000..1257f7f0d --- /dev/null +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -0,0 +1,59 @@ +.fr-grid-row.fr-grid-row--gutters + .fr-col-12.fr-col-md-8 + = form_with url: form_url, model: @export_template, local: true do |f| + = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) do |c| + - c.with_hint do + Indiquez le nom à utiliser pour ce modèle d'export + + - if groupe_instructeurs.many? + .fr-input-group + = f.label :groupe_instructeur_id, class: 'fr-label' do + = f.object.class.human_attribute_name(:groupe_instructeur_id) + = render EditableChamp::AsteriskMandatoryComponent.new + %span.fr-hint-text + Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ? + = f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select' + - else + = f.hidden_field :groupe_instructeur_id + + = f.hidden_field :kind + + .fr-input-group{ data: { controller: 'tiptap' } } + = f.label :tiptap_default_dossier_directory, class: "fr-label" + .editor.mt-2{ data: { tiptap_target: 'editor' } } + = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input' } + %ul.mt-2.flex.wrap.flex-gap-1 + - @export_template.specific_tags.each do |tag| + %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } + = tag[:libelle] + + .fr-input-group{ data: { controller: 'tiptap' } } + = f.label :tiptap_pdf_name, class: "fr-label" + .editor.mt-2{ data: { tiptap_target: 'editor' } } + = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input' } + %ul.mt-2.flex.wrap.flex-gap-1 + - @export_template.specific_tags.each do |tag| + %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } + = tag[:libelle] + + - if @all_pj.any? + %h3 Pieces justificatives + + - @all_pj.each do |pj| + .fr-input-group{ data: { controller: 'tiptap' } } + = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" + .editor.mt-2{ data: { tiptap_target: 'editor' } } + = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input' } + %ul.mt-2.flex.wrap.flex-gap-1 + - @export_template.specific_tags.each do |tag| + %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } + = tag[:libelle] + + + .fixed-footer + .fr-container + %ul.fr-btns-group.fr-btns-group--inline-md + %li + = f.submit "Enregistrer", class: "fr-btn" + %li + = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary" diff --git a/app/views/instructeurs/export_templates/edit.html.haml b/app/views/instructeurs/export_templates/edit.html.haml new file mode 100644 index 000000000..bd4fc02b4 --- /dev/null +++ b/app/views/instructeurs/export_templates/edit.html.haml @@ -0,0 +1,7 @@ += render partial: 'administrateurs/breadcrumbs', + locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)], + [t('.title')]] } +.fr-container + %h1 Mise à jour modèle d'export + + = render partial: 'form', locals: { form_url: instructeur_export_template_path(@procedure, @export_template), groupe_instructeurs: @groupe_instructeurs } diff --git a/app/views/instructeurs/export_templates/new.html.haml b/app/views/instructeurs/export_templates/new.html.haml new file mode 100644 index 000000000..eeff6baa9 --- /dev/null +++ b/app/views/instructeurs/export_templates/new.html.haml @@ -0,0 +1,6 @@ += render partial: 'administrateurs/breadcrumbs', + locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)], + [t('.title')]] } +.fr-container + %h1 Nouveau modèle d'export + = render partial: 'form', locals: { form_url: instructeur_export_templates_path, groupe_instructeurs: @groupe_instructeurs } diff --git a/config/locales/views/instructeurs/procedures/export_templates/fr.yml b/config/locales/views/instructeurs/procedures/export_templates/fr.yml new file mode 100644 index 000000000..6f570152e --- /dev/null +++ b/config/locales/views/instructeurs/procedures/export_templates/fr.yml @@ -0,0 +1,8 @@ +fr: + instructeurs: + export_templates: + new: + title: Nouveau modèle d'export + edit: + title: Modèle d'export + diff --git a/config/routes.rb b/config/routes.rb index cb068da33..a9af9ac9e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -450,6 +450,7 @@ Rails.application.routes.draw do resources :procedures, only: [:index, :show], param: :procedure_id do member do resources :archives, only: [:index, :create] + resources :export_templates, only: [:new, :create, :edit, :update] resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do resource :contact_information diff --git a/spec/controllers/instructeurs/export_templates_controller_spec.rb b/spec/controllers/instructeurs/export_templates_controller_spec.rb new file mode 100644 index 000000000..83adb32ac --- /dev/null +++ b/spec/controllers/instructeurs/export_templates_controller_spec.rb @@ -0,0 +1,120 @@ +describe Instructeurs::ExportTemplatesController, type: :controller do + before { sign_in(instructeur.user) } + let(:tiptap_pdf_name) { + { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } + ] + }.to_json + } + + let(:export_template_params) do + { + name: "coucou", + kind: "zip", + groupe_instructeur_id: groupe_instructeur.id, + tiptap_pdf_name: tiptap_pdf_name, + tiptap_default_dossier_directory: { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] } + ] + }.to_json, + "pjs" => + [ + { path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " _justif", "type" => "text" }] }] }, stable_id: "3" }, + { + path: + { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "cni_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] }, + stable_id: "5" + }, + { + path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "pj_repet_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] }, + stable_id: "10" + } + ] + } + end + + let(:instructeur) { create(:instructeur) } + let(:procedure) { create(:procedure, instructeurs: [instructeur]) } + let(:groupe_instructeur) { procedure.defaut_groupe_instructeur } + + describe '#create' do + let(:subject) { post :create, params: { procedure_id: procedure.id, export_template: export_template_params } } + + context 'with valid params' do + it 'redirect to some page' do + subject + expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:)) + expect(flash.notice).to eq "Le modèle d'export coucou a bien été créé" + end + end + + context 'with invalid params' do + let(:tiptap_pdf_name) { { content: "invalid" }.to_json } + it 'display error notification' do + subject + expect(flash.alert).to be_present + end + end + + context 'with procedure not accessible by current instructeur' do + let(:another_procedure) { create(:procedure) } + let(:subject) { post :create, params: { procedure_id: another_procedure.id, export_template: export_template_params } } + it 'raise exception' do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + describe '#edit' do + let(:export_template) { create(:export_template, groupe_instructeur:) } + let(:subject) { get :edit, params: { procedure_id: procedure.id, id: export_template.id } } + + it 'render edit' do + subject + expect(response).to render_template(:edit) + end + + context "with export_template not accessible by current instructeur" do + let(:another_groupe_instructeur) { create(:groupe_instructeur) } + let(:export_template) { create(:export_template, groupe_instructeur: another_groupe_instructeur) } + + it 'raise exception' do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + describe '#update' do + let(:export_template) { create(:export_template, groupe_instructeur:) } + let(:tiptap_pdf_name) { + { + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "exPort_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } + ] + }.to_json + } + + let(:subject) { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } } + + context 'with valid params' do + it 'redirect to some page' do + subject + expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:)) + expect(flash.notice).to eq "Le modèle d'export coucou a bien été modifié" + end + end + + context 'with invalid params' do + let(:tiptap_pdf_name) { { content: "invalid" }.to_json } + it 'display error notification' do + subject + expect(flash.alert).to be_present + end + end + end +end From a12d6b4af0b5781c2882e80356451263ac490a23 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Sun, 3 Mar 2024 11:56:08 +0100 Subject: [PATCH 11/59] preview content export --- app/assets/stylesheets/exports.scss | 71 +++++++++++++++++++ .../export_templates_controller.rb | 8 +++ .../export_templates/_form.html.haml | 20 +++++- .../preview.turbo_stream.haml | 2 + config/routes.rb | 6 +- 5 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 app/assets/stylesheets/exports.scss create mode 100644 app/views/instructeurs/export_templates/preview.turbo_stream.haml diff --git a/app/assets/stylesheets/exports.scss b/app/assets/stylesheets/exports.scss new file mode 100644 index 000000000..ff734fea2 --- /dev/null +++ b/app/assets/stylesheets/exports.scss @@ -0,0 +1,71 @@ +@import "constants"; + +.export-template-preview { + // From https://codepen.io/myramoki/pen/xZJjrr + .tree { + margin-left: 0; + } + + .tree, + .tree ul { + padding: 0; + list-style: none; + position: relative; + } + + .tree ul { + margin: 0 0 0 0.5em; // (indentation/2) + } + + .tree:before, + .tree ul:before { + content: ""; + display: block; + width: 0; + position: absolute; + top: 0; + bottom: 0; + left: 4px; + border-left: 1px dashed; + } + + ul.tree:before { + border-left: none; + } + + .tree li { + margin: 0; + padding: 0 1.5em; // indentation + .5em + line-height: 2em; // default list item's `line-height` + position: relative; + } + + .tree > li { + padding-left: 0; // Don't indent first level + } + + .tree li:before { + content: ""; + display: block; + width: 10px; // same with indentation + height: 0; + border-top: 1px dashed; + margin-top: -1px; // border top width + position: absolute; + top: 1em; // (line-height/2) + left: 4px; + } + + ul.tree > li:before { + border-top: none; + } + + .tree li:last-child:before { + background: var( + --background-alt-blue-france + ); // same with body background + height: auto; + top: 1em; // (line-height/2) + bottom: 0; + } +} diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb index 4b12a848b..e04cf8152 100644 --- a/app/controllers/instructeurs/export_templates_controller.rb +++ b/app/controllers/instructeurs/export_templates_controller.rb @@ -37,6 +37,14 @@ module Instructeurs end end + def preview + param = params.require(:export_template).keys.first + @preview_param = param.delete_prefix("tiptap_") + hash = JSON.parse(params[:export_template][param]).deep_symbolize_keys + export_template = ExportTemplate.new(kind: 'zip', groupe_instructeur: @groupe_instructeurs.first) + @preview_value = export_template.render_attributes_for(hash, @procedure.dossier_for_preview(current_instructeur)) + end + private def export_template_params diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 1257f7f0d..4ae51c1d5 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -21,7 +21,7 @@ .fr-input-group{ data: { controller: 'tiptap' } } = f.label :tiptap_default_dossier_directory, class: "fr-label" .editor.mt-2{ data: { tiptap_target: 'editor' } } - = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input' } + = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } %ul.mt-2.flex.wrap.flex-gap-1 - @export_template.specific_tags.each do |tag| %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } @@ -30,7 +30,7 @@ .fr-input-group{ data: { controller: 'tiptap' } } = f.label :tiptap_pdf_name, class: "fr-label" .editor.mt-2{ data: { tiptap_target: 'editor' } } - = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input' } + = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } %ul.mt-2.flex.wrap.flex-gap-1 - @export_template.specific_tags.each do |tag| %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } @@ -43,7 +43,7 @@ .fr-input-group{ data: { controller: 'tiptap' } } = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" .editor.mt-2{ data: { tiptap_target: 'editor' } } - = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input' } + = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } %ul.mt-2.flex.wrap.flex-gap-1 - @export_template.specific_tags.each do |tag| %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } @@ -57,3 +57,17 @@ = f.submit "Enregistrer", class: "fr-btn" %li = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary" + - if @export_template.sample_dossier + .fr-col-12.fr-col-md-4.fr-background-alt--blue-france + .export-template-preview.fr-p-2w.sticky--top + %h2.fr-h4 Aperçu + %ul.tree.fr-text--sm + %li= DownloadableFileService::EXPORT_DIRNAME + %li + %ul + %li + %span#preview_default_dossier_directory= @export_template.tiptap_convert(sample_dossier, "default_dossier_directory") + %ul + %li#preview_pdf_name= @export_template.tiptap_convert(sample_dossier, "pdf_name") + - @procedure.pieces_jointes_exportables_list.each do |pj| + %li{id: "preview_pj_#{pj.stable_id}"}= @export_template.tiptap_convert_pj(sample_dossier, pj.stable_id) diff --git a/app/views/instructeurs/export_templates/preview.turbo_stream.haml b/app/views/instructeurs/export_templates/preview.turbo_stream.haml new file mode 100644 index 000000000..f6c1e5468 --- /dev/null +++ b/app/views/instructeurs/export_templates/preview.turbo_stream.haml @@ -0,0 +1,2 @@ += turbo_stream.update "preview_#{@preview_param}" do + = @preview_value diff --git a/config/routes.rb b/config/routes.rb index a9af9ac9e..461575d5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -450,7 +450,11 @@ Rails.application.routes.draw do resources :procedures, only: [:index, :show], param: :procedure_id do member do resources :archives, only: [:index, :create] - resources :export_templates, only: [:new, :create, :edit, :update] + resources :export_templates, only: [:new, :create, :edit, :update] do + collection do + get 'preview' + end + end resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do resource :contact_information From 7661b8b1b2859a77296ba64016f5ff57a0f0ace5 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 8 Mar 2024 10:11:11 +0100 Subject: [PATCH 12/59] add export_template to exports --- app/models/export.rb | 1 + db/migrate/20240131094915_add_template_to_exports.rb | 6 ++++++ db/migrate/20240131095645_add_export_template_fk.rb | 5 +++++ db/migrate/20240131100329_validate_export_template_fk.rb | 5 +++++ db/schema.rb | 3 +++ 5 files changed, 20 insertions(+) create mode 100644 db/migrate/20240131094915_add_template_to_exports.rb create mode 100644 db/migrate/20240131095645_add_export_template_fk.rb create mode 100644 db/migrate/20240131100329_validate_export_template_fk.rb diff --git a/app/models/export.rb b/app/models/export.rb index 3a7a1ac34..66832d5e1 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -31,6 +31,7 @@ class Export < ApplicationRecord belongs_to :procedure_presentation, optional: true belongs_to :instructeur, optional: true belongs_to :user_profile, polymorphic: true, optional: true + belongs_to :export_template, optional: true has_one_attached :file diff --git a/db/migrate/20240131094915_add_template_to_exports.rb b/db/migrate/20240131094915_add_template_to_exports.rb new file mode 100644 index 000000000..397f053b7 --- /dev/null +++ b/db/migrate/20240131094915_add_template_to_exports.rb @@ -0,0 +1,6 @@ +class AddTemplateToExports < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + def change + add_reference :exports, :export_template, null: true, index: { algorithm: :concurrently } + end +end diff --git a/db/migrate/20240131095645_add_export_template_fk.rb b/db/migrate/20240131095645_add_export_template_fk.rb new file mode 100644 index 000000000..3e90045bc --- /dev/null +++ b/db/migrate/20240131095645_add_export_template_fk.rb @@ -0,0 +1,5 @@ +class AddExportTemplateFk < ActiveRecord::Migration[7.0] + def change + add_foreign_key :exports, :export_templates, validate: false + end +end diff --git a/db/migrate/20240131100329_validate_export_template_fk.rb b/db/migrate/20240131100329_validate_export_template_fk.rb new file mode 100644 index 000000000..08180880d --- /dev/null +++ b/db/migrate/20240131100329_validate_export_template_fk.rb @@ -0,0 +1,5 @@ +class ValidateExportTemplateFk < ActiveRecord::Migration[7.0] + def change + validate_foreign_key :exports, :export_templates + end +end diff --git a/db/schema.rb b/db/schema.rb index 054c32caa..22ee0febd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -606,6 +606,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do create_table "exports", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.integer "dossiers_count" + t.bigint "export_template_id" t.string "format", null: false t.bigint "instructeur_id" t.string "job_status", default: "pending", null: false @@ -617,6 +618,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do t.datetime "updated_at", precision: nil, null: false t.bigint "user_profile_id" t.string "user_profile_type" + t.index ["export_template_id"], name: "index_exports_on_export_template_id" t.index ["instructeur_id"], name: "index_exports_on_instructeur_id" t.index ["key"], name: "index_exports_on_key" t.index ["procedure_presentation_id"], name: "index_exports_on_procedure_presentation_id" @@ -1235,6 +1237,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_17_053843) do add_foreign_key "experts_procedures", "experts" add_foreign_key "experts_procedures", "procedures" add_foreign_key "export_templates", "groupe_instructeurs" + add_foreign_key "exports", "export_templates" add_foreign_key "exports", "instructeurs" add_foreign_key "france_connect_informations", "users" add_foreign_key "geo_areas", "champs" From 7a397526302c4ea872cdb03579e50b9306f2ad95 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 22 Mar 2024 10:05:47 +0100 Subject: [PATCH 13/59] rename root directory in zip export --- app/services/downloadable_file_service.rb | 5 +- .../procedure_archive_service_spec.rb | 50 +++++++++---------- .../services/procedure_export_service_spec.rb | 11 ++-- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/app/services/downloadable_file_service.rb b/app/services/downloadable_file_service.rb index 97909578b..c6d87e5b1 100644 --- a/app/services/downloadable_file_service.rb +++ b/app/services/downloadable_file_service.rb @@ -1,9 +1,10 @@ class DownloadableFileService ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' } + EXPORT_DIRNAME = 'export' def self.download_and_zip(procedure, attachments, filename, &block) Dir.mktmpdir(nil, ARCHIVE_CREATION_DIR) do |tmp_dir| - export_dir = File.join(tmp_dir, filename) + export_dir = File.join(tmp_dir, EXPORT_DIRNAME) zip_path = File.join(ARCHIVE_CREATION_DIR, "#{filename}.zip") begin @@ -15,7 +16,7 @@ class DownloadableFileService Dir.chdir(tmp_dir) do File.delete(zip_path) if File.exist?(zip_path) - system 'zip', '-0', '-r', zip_path, filename + system 'zip', '-0', '-r', zip_path, EXPORT_DIRNAME end yield(zip_path) ensure diff --git a/spec/services/procedure_archive_service_spec.rb b/spec/services/procedure_archive_service_spec.rb index 8edd88065..40b8caa9f 100644 --- a/spec/services/procedure_archive_service_spec.rb +++ b/spec/services/procedure_archive_service_spec.rb @@ -33,11 +33,11 @@ describe ProcedureArchiveService do files = ZipTricks::FileReader.read_zip_structure(io: f) structure = [ - "#{service.send(:zip_root_folder, archive)}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2021-00-00-#{dossier.attestation.pdf.id % 10000}.pdf", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf" + "export/", + "export/dossier-#{dossier.id}/", + "export/dossier-#{dossier.id}/pieces_justificatives/", + "export/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2021-00-00-#{dossier.attestation.pdf.id % 10000}.pdf", + "export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf" ] expect(files.map(&:filename)).to match_array(structure) end @@ -53,11 +53,11 @@ describe ProcedureArchiveService do archive.file.open do |f| files = ZipTricks::FileReader.read_zip_structure(io: f) structure = [ - "#{service.send(:zip_root_folder, archive)}/", - "#{service.send(:zip_root_folder, archive)}/-LISTE-DES-FICHIERS-EN-ERREURS.txt", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf" + "export/", + "export/-LISTE-DES-FICHIERS-EN-ERREURS.txt", + "export/dossier-#{dossier.id}/", + "export/dossier-#{dossier.id}/pieces_justificatives/", + "export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf" ] expect(files.map(&:filename)).to match_array(structure) end @@ -100,12 +100,12 @@ describe ProcedureArchiveService do archive.file.open do |f| zip_entries = ZipTricks::FileReader.read_zip_structure(io: f) structure = [ - "#{service.send(:zip_root_folder, archive)}/", - "#{service.send(:zip_root_folder, archive)}/-LISTE-DES-FICHIERS-EN-ERREURS.txt", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-dossier-05-03-2020-00-00-1.pdf", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf" + "export/", + "export/-LISTE-DES-FICHIERS-EN-ERREURS.txt", + "export/dossier-#{dossier.id}/", + "export/dossier-#{dossier.id}/export-dossier-05-03-2020-00-00-1.pdf", + "export/dossier-#{dossier.id}/pieces_justificatives/", + "export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id % 10000}.pdf" ] expect(zip_entries.map(&:filename)).to match_array(structure) zip_entries.map do |entry| @@ -134,15 +134,15 @@ describe ProcedureArchiveService do archive.file.open do |f| files = ZipTricks::FileReader.read_zip_structure(io: f) structure = [ - "#{service.send(:zip_root_folder, archive)}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier.attestation.pdf.id % 10000}.pdf", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2020-00-00-#{dossier.id % 10000}.pdf", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/export-#{dossier_2020.id}-05-03-2020-00-00-#{dossier_2020.id % 10000}.pdf", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/pieces_justificatives/", - "#{service.send(:zip_root_folder, archive)}/dossier-#{dossier_2020.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier_2020.attestation.pdf.id % 10000}.pdf" + "export/", + "export/dossier-#{dossier.id}/", + "export/dossier-#{dossier.id}/pieces_justificatives/", + "export/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier.attestation.pdf.id % 10000}.pdf", + "export/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2020-00-00-#{dossier.id % 10000}.pdf", + "export/dossier-#{dossier_2020.id}/", + "export/dossier-#{dossier_2020.id}/export-#{dossier_2020.id}-05-03-2020-00-00-#{dossier_2020.id % 10000}.pdf", + "export/dossier-#{dossier_2020.id}/pieces_justificatives/", + "export/dossier-#{dossier_2020.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier_2020.attestation.pdf.id % 10000}.pdf" ] expect(files.map(&:filename)).to match_array(structure) end diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index 7f783eafd..aa2fb3733 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -540,12 +540,13 @@ describe ProcedureExportService do File.write('tmp.zip', subject.download, mode: 'wb') File.open('tmp.zip') do |fd| files = ZipTricks::FileReader.read_zip_structure(io: fd) + base_fn = 'export' structure = [ - "#{service.send(:base_filename)}/", - "#{service.send(:base_filename)}/dossier-#{dossier.id}/", - "#{service.send(:base_filename)}/dossier-#{dossier.id}/pieces_justificatives/", - "#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", - "#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" + "#{base_fn}/", + "#{base_fn}/dossier-#{dossier.id}/", + "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", + "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", + "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" ] expect(files.size).to eq(structure.size) expect(files.map(&:filename)).to match_array(structure) From 357c07456cfd0d578bd3a0c8e4bcb03635dede43 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 8 Mar 2024 17:14:21 +0100 Subject: [PATCH 14/59] generate export with export_template --- app/controllers/api/v2/dossiers_controller.rb | 2 +- .../instructeurs/dossiers_controller.rb | 2 +- .../instructeurs/procedures_controller.rb | 7 +- app/controllers/users/dossiers_controller.rb | 2 +- app/lib/active_storage/downloadable_file.rb | 4 +- .../parallel_download_queue.rb | 2 + app/models/champ.rb | 8 ++ app/models/export.rb | 5 +- app/services/pieces_justificatives_service.rb | 18 +++- app/services/procedure_export_service.rb | 5 +- .../experts/avis_controller_spec.rb | 2 +- .../instructeurs/dossiers_controller_spec.rb | 2 +- .../procedures_controller_spec.rb | 12 +++ .../users/dossiers_controller_spec.rb | 2 +- spec/models/export_spec.rb | 8 ++ .../pieces_justificatives_service_spec.rb | 12 ++- .../services/procedure_export_service_spec.rb | 86 +++++++++++++------ 17 files changed, 131 insertions(+), 48 deletions(-) diff --git a/app/controllers/api/v2/dossiers_controller.rb b/app/controllers/api/v2/dossiers_controller.rb index 0612aaf53..3c627bd36 100644 --- a/app/controllers/api/v2/dossiers_controller.rb +++ b/app/controllers/api/v2/dossiers_controller.rb @@ -2,7 +2,7 @@ class API::V2::DossiersController < API::V2::BaseController before_action :ensure_dossier_present def pdf - @acls = PiecesJustificativesService.new(user_profile: Administrateur.new).acl_for_dossier_export(dossier.procedure) + @acls = PiecesJustificativesService.new(user_profile: Administrateur.new, export_template: nil).acl_for_dossier_export(dossier.procedure) render(template: 'dossiers/show', formats: [:pdf]) end diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 04085f7c2..7ab9a717d 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -45,7 +45,7 @@ module Instructeurs @is_dossier_in_batch_operation = dossier.batch_operation.present? respond_to do |format| format.pdf do - @acls = PiecesJustificativesService.new(user_profile: current_instructeur).acl_for_dossier_export(dossier.procedure) + @acls = PiecesJustificativesService.new(user_profile: current_instructeur, export_template: nil).acl_for_dossier_export(dossier.procedure) render(template: 'dossiers/show', formats: [:pdf]) end format.all diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 8caf08fee..1559dca8c 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -324,13 +324,18 @@ module Instructeurs end def export_format - @export_format ||= params[:export_format] + @export_format ||= params[:export_format].presence || export_template&.kind + end + + def export_template + @export_template ||= ExportTemplate.find(params[:export_template_id]) if params[:export_template_id].present? end def export_options @export_options ||= { time_span_type: params[:time_span_type], statut: params[:statut], + export_template:, procedure_presentation: params[:statut].present? ? procedure_presentation : nil }.compact end diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 6176ce7f3..db49503c7 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -88,7 +88,7 @@ module Users end def show - pj_service = PiecesJustificativesService.new(user_profile: current_user) + pj_service = PiecesJustificativesService.new(user_profile: current_user, export_template: nil) respond_to do |format| format.pdf do @dossier = dossier_with_champs(pj_template: false) diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb index b89c20997..57e919ff3 100644 --- a/app/lib/active_storage/downloadable_file.rb +++ b/app/lib/active_storage/downloadable_file.rb @@ -1,8 +1,8 @@ require 'fog/openstack' class ActiveStorage::DownloadableFile - def self.create_list_from_dossiers(dossiers:, user_profile:) - pj_service = PiecesJustificativesService.new(user_profile:) + def self.create_list_from_dossiers(dossiers:, user_profile:, export_template: nil) + pj_service = PiecesJustificativesService.new(user_profile:, export_template:) pj_service.generate_dossiers_export(dossiers) + pj_service.liste_documents(dossiers) end diff --git a/app/lib/download_manager/parallel_download_queue.rb b/app/lib/download_manager/parallel_download_queue.rb index 16dcbd762..35a6b8695 100644 --- a/app/lib/download_manager/parallel_download_queue.rb +++ b/app/lib/download_manager/parallel_download_queue.rb @@ -12,6 +12,8 @@ module DownloadManager end def download_all + # TODO: arriver à enelver ce parametrage d'ActiveStorage + ActiveStorage::Current.url_options = { host: ENV.fetch("APP_HOST") } hydra = Typhoeus::Hydra.new(max_concurrency: DOWNLOAD_MAX_PARALLEL) attachments.each do |attachment, path| diff --git a/app/models/champ.rb b/app/models/champ.rb index 2cb0bb4ec..6d3f02f11 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -91,6 +91,14 @@ class Champ < ApplicationRecord parent_id.present? end + def stable_id_with_row + [row_id, stable_id].compact + end + + def row_index + Champ.where(parent:).pluck(:row_id).sort.index(:id) + end + # used for the `required` html attribute # check visibility to avoid hidden required input # which prevent the form from being sent. diff --git a/app/models/export.rb b/app/models/export.rb index 66832d5e1..d9b29d409 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -67,9 +67,10 @@ class Export < ApplicationRecord procedure_presentation_id.present? end - def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil) + def self.find_or_create_fresh_export(format, groupe_instructeurs, user_profile, time_span_type: time_span_types.fetch(:everything), statut: statuts.fetch(:tous), procedure_presentation: nil, export_template: nil) attributes = { format:, + export_template:, time_span_type:, statut:, key: generate_cache_key(groupe_instructeurs.map(&:id), procedure_presentation) @@ -148,7 +149,7 @@ class Export < ApplicationRecord end def blob - service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile) + service = ProcedureExportService.new(procedure, dossiers_for_export, user_profile, export_template) case format.to_sym when :csv diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 2fece0c0b..22040d4e0 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -1,6 +1,7 @@ class PiecesJustificativesService - def initialize(user_profile:) + def initialize(user_profile:, export_template:) @user_profile = user_profile + @export_template = export_template end def liste_documents(dossiers) @@ -58,7 +59,11 @@ class PiecesJustificativesService created_at: dossier.updated_at ) - pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a) + if @export_template + pdfs << @export_template.attachment_and_path(dossier, a) + else + pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a) + end end pdfs @@ -153,9 +158,14 @@ class PiecesJustificativesService .includes(:blob) .where(record_type: "Champ", record_id: champ_id_dossier_id.keys) .filter { |a| safe_attachment(a) } - .map do |a| + .map do |a, _i| dossier_id = champ_id_dossier_id[a.record_id] - ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) + pj_index = Champ.find(a.record_id).piece_justificative_file.blobs.map(&:id).index(a.blob_id) + if @export_template + @export_template.attachment_and_path(Dossier.find(dossier_id), a, index: pj_index, row_index: a.record.row_index) + else + ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) + end end end diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index 75eab89e8..5503a8a14 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -1,10 +1,11 @@ class ProcedureExportService attr_reader :procedure, :dossiers - def initialize(procedure, dossiers, user_profile) + def initialize(procedure, dossiers, user_profile, export_template) @procedure = procedure @dossiers = dossiers @user_profile = user_profile + @export_template = export_template end def to_csv @@ -36,7 +37,7 @@ class ProcedureExportService end def to_zip - attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers:, user_profile: @user_profile) + attachments = ActiveStorage::DownloadableFile.create_list_from_dossiers(dossiers:, user_profile: @user_profile, export_template: @export_template) DownloadableFileService.download_and_zip(procedure, attachments, base_filename) do |zip_filepath| ArchiveUploader.new(procedure: procedure, filename: filename(:zip), filepath: zip_filepath).blob diff --git a/spec/controllers/experts/avis_controller_spec.rb b/spec/controllers/experts/avis_controller_spec.rb index 7a4029eed..e58b4d2bb 100644 --- a/spec/controllers/experts/avis_controller_spec.rb +++ b/spec/controllers/experts/avis_controller_spec.rb @@ -121,7 +121,7 @@ describe Experts::AvisController, type: :controller do context 'with a valid avis' do it do service = instance_double(PiecesJustificativesService) - expect(PiecesJustificativesService).to receive(:new).with(user_profile: expert).and_return(service) + expect(PiecesJustificativesService).to receive(:new).with(user_profile: expert, export_template: nil).and_return(service) expect(service).to receive(:generate_dossiers_export).with(Dossier.where(id: dossier)).and_return([]) expect(service).to receive(:liste_documents).with(Dossier.where(id: dossier)).and_return([]) is_expected.to have_http_status(:success) diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 8b99ff44f..8add60a04 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -936,7 +936,7 @@ describe Instructeurs::DossiersController, type: :controller do subject end - it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: instructeur).acl_for_dossier_export(dossier.procedure)) } + it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: instructeur, export_template: nil).acl_for_dossier_export(dossier.procedure)) } it { expect(assigns(:is_dossier_in_batch_operation)).to eq(false) } it { expect(response).to render_template 'dossiers/show' } diff --git a/spec/controllers/instructeurs/procedures_controller_spec.rb b/spec/controllers/instructeurs/procedures_controller_spec.rb index ca0e1a4a9..3f34c3e53 100644 --- a/spec/controllers/instructeurs/procedures_controller_spec.rb +++ b/spec/controllers/instructeurs/procedures_controller_spec.rb @@ -736,6 +736,18 @@ describe Instructeurs::ProceduresController, type: :controller do end it { expect { subject }.to change { Export.where(user_profile: instructeur).count }.by(1) } + + context 'with an export template' do + let(:export_template) { create(:export_template) } + subject do + get :download_export, params: { export_template_id: export_template.id, procedure_id: procedure.id } + end + + it 'displays an notice' do + is_expected.to redirect_to(exports_instructeur_procedure_url(procedure)) + expect(flash.notice).to be_present + end + end end context 'when the export is not ready' do diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index e0fd3501e..fb1a28e53 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -1142,7 +1142,7 @@ describe Users::DossiersController, type: :controller do end context 'when the dossier has been submitted' do - it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: user).acl_for_dossier_export(dossier.procedure)) } + it { expect(assigns(:acls)).to eq(PiecesJustificativesService.new(user_profile: user, export_template: nil).acl_for_dossier_export(dossier.procedure)) } it { expect(response).to render_template('dossiers/show') } end end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 5e7e1eda3..5b2a8ae99 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -109,6 +109,14 @@ RSpec.describe Export, type: :model do end end + context 'with export template' do + let(:export_template) { build(:export_template) } + it 'creates new export' do + expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, export_template: export_template, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) } + .to change { Export.count }.by(1) + end + end + context 'with existing matching export' do def find_or_create = Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 7a212912e..771cb8e96 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -2,8 +2,9 @@ describe PiecesJustificativesService do describe '.liste_documents' do let(:dossier) { create(:dossier, procedure: procedure) } let(:dossiers) { Dossier.where(id: dossier.id) } + let(:export_template) { nil } subject do - PiecesJustificativesService.new(user_profile:).liste_documents(dossiers).map(&:first) + PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:first) end context 'no acl' do @@ -19,6 +20,11 @@ describe PiecesJustificativesService do end it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + + context 'with export_template' do + let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } + it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + end end context 'with a multiple attachments' do @@ -303,7 +309,7 @@ describe PiecesJustificativesService do let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :piece_justificative }] }]) } let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) } let(:dossiers) { Dossier.where(id: dossier.id) } - subject { PiecesJustificativesService.new(user_profile:).generate_dossiers_export(dossiers) } + subject { PiecesJustificativesService.new(user_profile:, export_template: nil).generate_dossiers_export(dossiers) } it "doesn't update dossier" do expect { subject }.not_to change { dossier.updated_at } @@ -315,7 +321,7 @@ describe PiecesJustificativesService do let!(:not_confidentiel_avis) { create(:avis, :not_confidentiel, dossier: dossier) } let!(:expert_avis) { create(:avis, :confidentiel, dossier: dossier, expert: user_profile) } - subject { PiecesJustificativesService.new(user_profile:).generate_dossiers_export(dossiers) } + subject { PiecesJustificativesService.new(user_profile:, export_template: nil).generate_dossiers_export(dossiers) } it "includes avis not confidentiel as well as expert's avis" do expect_any_instance_of(Dossier).to receive(:avis_for_expert).with(user_profile).and_return([]) subject diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index aa2fb3733..9109124f4 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -2,8 +2,9 @@ require 'csv' describe ProcedureExportService do let(:instructeur) { create(:instructeur) } - let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs, instructeurs: [instructeur]) } - let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur) } + let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } + let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } + let(:export_template) { nil } describe 'to_xlsx' do subject do @@ -243,7 +244,7 @@ describe ProcedureExportService do context 'as csv' do subject do - ProcedureExportService.new(procedure, procedure.dossiers, instructeur) + ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) .to_csv .open { |f| CSV.read(f.path) } end @@ -519,39 +520,68 @@ describe ProcedureExportService do end end - context 'generate_dossiers_export' do + describe 'generate_dossiers_export' do it 'include_infos_administration (so it includes avis, champs privés)' do - expect(ActiveStorage::DownloadableFile).to receive(:create_list_from_dossiers).with(dossiers: anything, user_profile: instructeur).and_return([]) + expect(ActiveStorage::DownloadableFile).to receive(:create_list_from_dossiers).with(dossiers: anything, user_profile: instructeur, export_template:).and_return([]) subject end - end - context 'with files (and http calls)' do - let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } - let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur).generate_dossiers_export(Dossier.where(id: dossier)) } - before do - allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + context 'with export_template' do + let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } + let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) } + let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } + before do + allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + end + + it 'returns a blob with custom filenames' do + VCR.use_cassette('archive/new_file_to_get_200') do + subject + File.write('tmp.zip', subject.download, mode: 'wb') + File.open('tmp.zip') do |fd| + files = ZipTricks::FileReader.read_zip_structure(io: fd) + base_fn = "export" + structure = [ + "#{base_fn}/", + "#{base_fn}/dossier-#{dossier.id}/", + "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}.txt", + "#{base_fn}/dossier-#{dossier.id}/export_#{dossier.id}.pdf" + ] + expect(files.size).to eq(structure.size) + expect(files.map(&:filename)).to match_array(structure) + end + FileUtils.remove_entry_secure('tmp.zip') + end + end end - it 'returns a blob with valid files' do - VCR.use_cassette('archive/new_file_to_get_200') do - subject + context 'with files (and http calls)' do + let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } + let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template: nil).generate_dossiers_export(Dossier.where(id: dossier)) } + before do + allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + end - File.write('tmp.zip', subject.download, mode: 'wb') - File.open('tmp.zip') do |fd| - files = ZipTricks::FileReader.read_zip_structure(io: fd) - base_fn = 'export' - structure = [ - "#{base_fn}/", - "#{base_fn}/dossier-#{dossier.id}/", - "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", - "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", - "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" - ] - expect(files.size).to eq(structure.size) - expect(files.map(&:filename)).to match_array(structure) + it 'returns a blob with valid files' do + VCR.use_cassette('archive/new_file_to_get_200') do + subject + + File.write('tmp.zip', subject.download, mode: 'wb') + File.open('tmp.zip') do |fd| + files = ZipTricks::FileReader.read_zip_structure(io: fd) + base_fn = 'export' + structure = [ + "#{base_fn}/", + "#{base_fn}/dossier-#{dossier.id}/", + "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", + "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", + "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" + ] + expect(files.size).to eq(structure.size) + expect(files.map(&:filename)).to match_array(structure) + end + FileUtils.remove_entry_secure('tmp.zip') end - FileUtils.remove_entry_secure('tmp.zip') end end end From 2a4bfdd40be75b0d44b0dbb59314e63997817831 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 8 Mar 2024 17:15:47 +0100 Subject: [PATCH 15/59] can use export template from export_dropdown_component --- app/components/dossiers/export_dropdown_component.rb | 10 ++++++++-- .../export_dropdown_component.html.haml | 4 ++++ .../administrateurs/exports/download.turbo_stream.haml | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/components/dossiers/export_dropdown_component.rb b/app/components/dossiers/export_dropdown_component.rb index 098dae369..bf67f688e 100644 --- a/app/components/dossiers/export_dropdown_component.rb +++ b/app/components/dossiers/export_dropdown_component.rb @@ -3,6 +3,7 @@ class Dossiers::ExportDropdownComponent < ApplicationComponent def initialize(procedure:, statut: nil, count: nil, class_btn: nil, export_url: nil) @procedure = procedure + @export_templates = procedure.export_templates @statut = statut @count = count @class_btn = class_btn @@ -21,10 +22,15 @@ class Dossiers::ExportDropdownComponent < ApplicationComponent item.fetch(:format) != :json || @procedure.active_revision.carte? end - def download_export_path(export_format:, no_progress_notification: nil) + def download_export_path(export_format: nil, export_template_id: nil, no_progress_notification: nil) @export_url.call(@procedure, - export_format: export_format, + export_format:, + export_template_id:, statut: @statut, no_progress_notification: no_progress_notification) end + + def export_templates + @export_templates + end end diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml index 12e064ae0..398a9571d 100644 --- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml +++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml @@ -14,3 +14,7 @@ - menu.with_item do = link_to download_export_path(export_format: format), role: 'menuitem', data: { turbo_method: :post, turbo: true } do = t(".everything_#{format}_html") + - export_templates.each do |export_template| + - menu.with_item do + = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do + = "Exporter à partir du modèle #{export_template.name}" diff --git a/app/views/administrateurs/exports/download.turbo_stream.haml b/app/views/administrateurs/exports/download.turbo_stream.haml index 6db447783..e88340126 100644 --- a/app/views/administrateurs/exports/download.turbo_stream.haml +++ b/app/views/administrateurs/exports/download.turbo_stream.haml @@ -1,4 +1,4 @@ -# not renderable as administrateur flagged as manager, so render it anyway - if @can_download_dossiers = turbo_stream.update_all '.procedure-actions' do - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, count: @dossiers_count, export_url: method(:admin_procedure_exports_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates, count: @dossiers_count, export_url: method(:admin_procedure_exports_path)) From fd9335f12989dfa6fc8086bd12d7511c52babba5 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 11 Mar 2024 15:56:17 +0100 Subject: [PATCH 16/59] style editor tags --- .../tags_button_list_component.html.haml | 19 ++++++++++--------- .../export_templates/_form.html.haml | 16 +++------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/app/components/tags_button_list_component/tags_button_list_component.html.haml b/app/components/tags_button_list_component/tags_button_list_component.html.haml index 74f66a55d..ee52a1c09 100644 --- a/app/components/tags_button_list_component/tags_button_list_component.html.haml +++ b/app/components/tags_button_list_component/tags_button_list_component.html.haml @@ -1,14 +1,15 @@ - each_category do |category, tags, can_toggle_nullable| - .flex - %p.fr-label.fr-text--sm.fr-text--bold.fr-mb-1w= t(category, scope: ".categories") + - if category.present? + .flex + %p.fr-label.fr-text--sm.fr-text--bold.fr-mb-1w= t(category, scope: ".categories") - - if can_toggle_nullable - .fr-fieldset__element.fr-ml-4w - .fr-checkbox-group.fr-checkbox-group--sm - = check_box_tag("show_maybe_null", 1, false, data: { "no-autosubmit" => true, action: "change->attestation#toggleMaybeNull"}) - = label_tag "show_maybe_null", for: :show_maybe_null do - Voir les champs facultatifs - %span.hidden.fr-hint-text Un champ non rempli restera vide dans l’attestation. + - if can_toggle_nullable + .fr-fieldset__element.fr-ml-4w + .fr-checkbox-group.fr-checkbox-group--sm + = check_box_tag("show_maybe_null", 1, false, data: { "no-autosubmit" => true, action: "change->attestation#toggleMaybeNull"}) + = label_tag "show_maybe_null", for: :show_maybe_null do + Voir les champs facultatifs + %span.hidden.fr-hint-text Un champ non rempli restera vide dans l’attestation. %ul.fr-tags-group{ data: { category: category } } - tags.each do |tag| diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 4ae51c1d5..2acdb655f 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -22,19 +22,13 @@ = f.label :tiptap_default_dossier_directory, class: "fr-label" .editor.mt-2{ data: { tiptap_target: 'editor' } } = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } - %ul.mt-2.flex.wrap.flex-gap-1 - - @export_template.specific_tags.each do |tag| - %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } - = tag[:libelle] + .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) .fr-input-group{ data: { controller: 'tiptap' } } = f.label :tiptap_pdf_name, class: "fr-label" .editor.mt-2{ data: { tiptap_target: 'editor' } } = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } - %ul.mt-2.flex.wrap.flex-gap-1 - - @export_template.specific_tags.each do |tag| - %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } - = tag[:libelle] + .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) - if @all_pj.any? %h3 Pieces justificatives @@ -44,11 +38,7 @@ = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" .editor.mt-2{ data: { tiptap_target: 'editor' } } = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } - %ul.mt-2.flex.wrap.flex-gap-1 - - @export_template.specific_tags.each do |tag| - %li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } } - = tag[:libelle] - + .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) .fixed-footer .fr-container From 5aac2ecdc45e4065c618e225538049c925af1388 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Mon, 11 Mar 2024 16:20:00 +0100 Subject: [PATCH 17/59] rename editor css class Co-authored-by: Colin Darie --- .../stylesheets/attestation_template_2_edit.scss | 13 +------------ app/assets/stylesheets/tiptap_editor.scss | 14 ++++++++++++++ .../attestation_template_v2s/edit.html.haml | 2 +- .../instructeurs/export_templates/_form.html.haml | 6 +++--- 4 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 app/assets/stylesheets/tiptap_editor.scss diff --git a/app/assets/stylesheets/attestation_template_2_edit.scss b/app/assets/stylesheets/attestation_template_2_edit.scss index b726e9a8d..74a0a5684 100644 --- a/app/assets/stylesheets/attestation_template_2_edit.scss +++ b/app/assets/stylesheets/attestation_template_2_edit.scss @@ -20,7 +20,7 @@ min-height: 400px; } - .editor { + .tiptap-editor { // Visual zones .header .flex-1, h1 { @@ -63,17 +63,6 @@ li p { margin-bottom: 0; } - - // Tags - .fr-menu__list { - max-height: 500px; - } - - .fr-tag:not(.fr-menu .fr-tag) { - // style span rendered by tiptap like a button/link tag - color: var(--text-action-high-blue-france); - background-color: var(--background-action-low-blue-france); - } } // scss-lint:disable SelectorFormat diff --git a/app/assets/stylesheets/tiptap_editor.scss b/app/assets/stylesheets/tiptap_editor.scss new file mode 100644 index 000000000..9682989e5 --- /dev/null +++ b/app/assets/stylesheets/tiptap_editor.scss @@ -0,0 +1,14 @@ +@import "constants"; + +.tiptap-editor { + // Tags + .fr-menu__list { + max-height: 500px; + } + + .fr-tag:not(.fr-menu .fr-tag) { + // style span rendered by tiptap like a button/link tag + color: var(--text-action-high-blue-france); + background-color: var(--background-action-low-blue-france); + } +} diff --git a/app/views/administrateurs/attestation_template_v2s/edit.html.haml b/app/views/administrateurs/attestation_template_v2s/edit.html.haml index 308dca5e4..090247519 100644 --- a/app/views/administrateurs/attestation_template_v2s/edit.html.haml +++ b/app/views/administrateurs/attestation_template_v2s/edit.html.haml @@ -77,7 +77,7 @@ %button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } } = label - #editor.editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} } + #editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} } = f.hidden_field :tiptap_body, data: { tiptap_target: 'input' } .fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) } diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 2acdb655f..30dde872f 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -20,13 +20,13 @@ .fr-input-group{ data: { controller: 'tiptap' } } = f.label :tiptap_default_dossier_directory, class: "fr-label" - .editor.mt-2{ data: { tiptap_target: 'editor' } } + .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) .fr-input-group{ data: { controller: 'tiptap' } } = f.label :tiptap_pdf_name, class: "fr-label" - .editor.mt-2{ data: { tiptap_target: 'editor' } } + .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) @@ -36,7 +36,7 @@ - @all_pj.each do |pj| .fr-input-group{ data: { controller: 'tiptap' } } = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" - .editor.mt-2{ data: { tiptap_target: 'editor' } } + .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) From 4a79ecf301ec6cd1bc527d0f0e247c68edd8090e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 12 Mar 2024 13:44:22 +0100 Subject: [PATCH 18/59] add new export template link --- .../export_dropdown_component.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml index 398a9571d..2583997c4 100644 --- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml +++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml @@ -18,3 +18,6 @@ - menu.with_item do = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do = "Exporter à partir du modèle #{export_template.name}" + - menu.with_item do + = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do + Ajouter un modèle d'export From be0c0311c5a9407367784901b9b80984ed282f11 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 12 Mar 2024 15:39:29 +0100 Subject: [PATCH 19/59] use export template for admin archives --- app/controllers/administrateurs/exports_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/administrateurs/exports_controller.rb b/app/controllers/administrateurs/exports_controller.rb index 6e1d55305..c7cd0a751 100644 --- a/app/controllers/administrateurs/exports_controller.rb +++ b/app/controllers/administrateurs/exports_controller.rb @@ -34,7 +34,11 @@ module Administrateurs private def export_format - @export_format ||= params[:export_format] + @export_format ||= params[:export_format].presence || export_template&.kind + end + + def export_template + @export_template ||= ExportTemplate.find(params[:export_template_id]) if params[:export_template_id].present? end def export_options From 93f1fd5ebf81a19d52f13218af57b6fbffb4da38 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 12 Mar 2024 16:21:15 +0100 Subject: [PATCH 20/59] add export template lists --- .../instructeurs/procedures_controller.rb | 1 + .../instructeurs/procedures/exports.html.haml | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 1559dca8c..386114576 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -245,6 +245,7 @@ module Instructeurs def exports @procedure = procedure @exports = Export.for_groupe_instructeurs(groupe_instructeur_ids).ante_chronological + @export_templates = current_instructeur.export_templates_for(@procedure).includes(:groupe_instructeur) cookies.encrypted[cookies_export_key] = { value: DateTime.current, expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml index ed2f67fa8..67bd7e42c 100644 --- a/app/views/instructeurs/procedures/exports.html.haml +++ b/app/views/instructeurs/procedures/exports.html.haml @@ -22,3 +22,25 @@ - else = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i ) + + .fr-table.fr-mt-5w + %table + %caption Liste des modèles d'export + %thead + %tr + %th{ scope: 'col' } Nom du modèle + %th{ scope: 'col' } Nom du modèle + %th{ scope: 'col' }= "Groupe instructeur" if @procedure.groupe_instructeurs.many? + %tbody + - @export_templates.each do |export_template| + %tr + %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) + %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many? + - @export_templates.each do |export_template| + %tr + %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) + %td= export_template.groupe_instructeur.label + + %p + = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do + Ajouter un modèle d'export From 2c28d97f3f7834d4d4237086f03c5bf018c60ff9 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 12 Mar 2024 17:28:43 +0100 Subject: [PATCH 21/59] destroy export_template --- .../instructeurs/export_templates_controller.rb | 8 ++++++++ app/models/export_template.rb | 1 + .../instructeurs/export_templates/_form.html.haml | 6 +++++- config/routes.rb | 2 +- .../export_templates_controller_spec.rb | 13 +++++++++++++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb index e04cf8152..9065b2d21 100644 --- a/app/controllers/instructeurs/export_templates_controller.rb +++ b/app/controllers/instructeurs/export_templates_controller.rb @@ -37,6 +37,14 @@ module Instructeurs end end + def destroy + if @export_template.destroy + redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été supprimé" + else + redirect_to exports_instructeur_procedure_path(procedure: @procedure), alert: "Le modèle d'export #{@export_template.name} n'a pu être supprimé" + end + end + def preview param = params.require(:export_template).keys.first @preview_param = param.delete_prefix("tiptap_") diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 2a7282fdc..8dc01aa85 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -3,6 +3,7 @@ class ExportTemplate < ApplicationRecord belongs_to :groupe_instructeur has_one :procedure, through: :groupe_instructeur + has_many :exports, dependent: :nullify validates_with ExportTemplateValidator DOSSIER_STATE = Dossier.states.fetch(:en_construction) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 30dde872f..057120f28 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -47,7 +47,11 @@ = f.submit "Enregistrer", class: "fr-btn" %li = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary" - - if @export_template.sample_dossier + - if @export_template.persisted? + %li + = link_to "Supprimer", instructeur_export_template_path(@export_template, procedure_id: @procedure.id), method: :delete, data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"}, class: "fr-btn fr-btn--secondary" + - sample_dossier = @procedure.dossier_for_preview(current_instructeur) + - if sample_dossier .fr-col-12.fr-col-md-4.fr-background-alt--blue-france .export-template-preview.fr-p-2w.sticky--top %h2.fr-h4 Aperçu diff --git a/config/routes.rb b/config/routes.rb index 461575d5b..3905bb24f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -450,7 +450,7 @@ Rails.application.routes.draw do resources :procedures, only: [:index, :show], param: :procedure_id do member do resources :archives, only: [:index, :create] - resources :export_templates, only: [:new, :create, :edit, :update] do + resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do collection do get 'preview' end diff --git a/spec/controllers/instructeurs/export_templates_controller_spec.rb b/spec/controllers/instructeurs/export_templates_controller_spec.rb index 83adb32ac..8b8d73b82 100644 --- a/spec/controllers/instructeurs/export_templates_controller_spec.rb +++ b/spec/controllers/instructeurs/export_templates_controller_spec.rb @@ -117,4 +117,17 @@ describe Instructeurs::ExportTemplatesController, type: :controller do end end end + + describe '#destroy' do + let(:export_template) { create(:export_template, groupe_instructeur:) } + let(:subject) { delete :destroy, params: { procedure_id: procedure.id, id: export_template.id } } + + context 'with valid params' do + it 'redirect to some page' do + subject + expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:)) + expect(flash.notice).to eq "Le modèle d'export Mon export a bien été supprimé" + end + end + end end From 43c862ed4dc4c1391b1cf0ee857ed7af9be1868e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 13 Mar 2024 13:51:22 +0100 Subject: [PATCH 22/59] list export templates for groupes instructeur of current instructeur --- .../dossiers/export_dropdown_component.rb | 4 ++-- .../export_dropdown_component.html.haml | 14 ++++++++------ app/models/instructeur.rb | 4 ++++ .../exports/download.turbo_stream.haml | 2 +- .../procedures/deleted_dossiers.html.haml | 2 +- .../procedures/download_export.turbo_stream.haml | 4 ++-- app/views/instructeurs/procedures/show.html.haml | 4 ++-- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/components/dossiers/export_dropdown_component.rb b/app/components/dossiers/export_dropdown_component.rb index bf67f688e..91a3de116 100644 --- a/app/components/dossiers/export_dropdown_component.rb +++ b/app/components/dossiers/export_dropdown_component.rb @@ -1,9 +1,9 @@ class Dossiers::ExportDropdownComponent < ApplicationComponent include ApplicationHelper - def initialize(procedure:, statut: nil, count: nil, class_btn: nil, export_url: nil) + def initialize(procedure:, export_templates: nil, statut: nil, count: nil, class_btn: nil, export_url: nil) @procedure = procedure - @export_templates = procedure.export_templates + @export_templates = export_templates @statut = statut @count = count @class_btn = class_btn diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml index 2583997c4..dbbdbcb07 100644 --- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml +++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml @@ -14,10 +14,12 @@ - menu.with_item do = link_to download_export_path(export_format: format), role: 'menuitem', data: { turbo_method: :post, turbo: true } do = t(".everything_#{format}_html") - - export_templates.each do |export_template| + + - if export_templates.present? + - export_templates.each do |export_template| + - menu.with_item do + = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do + = "Exporter à partir du modèle #{export_template.name}" - menu.with_item do - = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do - = "Exporter à partir du modèle #{export_template.name}" - - menu.with_item do - = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do - Ajouter un modèle d'export + = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do + Ajouter un modèle d'export diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index 0b724d337..5933df7b7 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -303,6 +303,10 @@ class Instructeur < ApplicationRecord agent_connect_information.order(updated_at: :desc).first end + def export_templates_for(procedure) + procedure.export_templates.where(groupe_instructeur: groupe_instructeurs).order(:name) + end + private def annotations_hash(demande, annotations_privees, avis, messagerie) diff --git a/app/views/administrateurs/exports/download.turbo_stream.haml b/app/views/administrateurs/exports/download.turbo_stream.haml index e88340126..6db447783 100644 --- a/app/views/administrateurs/exports/download.turbo_stream.haml +++ b/app/views/administrateurs/exports/download.turbo_stream.haml @@ -1,4 +1,4 @@ -# not renderable as administrateur flagged as manager, so render it anyway - if @can_download_dossiers = turbo_stream.update_all '.procedure-actions' do - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates, count: @dossiers_count, export_url: method(:admin_procedure_exports_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, count: @dossiers_count, export_url: method(:admin_procedure_exports_path)) diff --git a/app/views/instructeurs/procedures/deleted_dossiers.html.haml b/app/views/instructeurs/procedures/deleted_dossiers.html.haml index b3b31961f..7482f95f9 100644 --- a/app/views/instructeurs/procedures/deleted_dossiers.html.haml +++ b/app/views/instructeurs/procedures/deleted_dossiers.html.haml @@ -11,7 +11,7 @@ .procedure-actions - if @can_download_dossiers - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path)) .fr-container.flex= render partial: "tabs", locals: { procedure: @procedure, statut: @statut, diff --git a/app/views/instructeurs/procedures/download_export.turbo_stream.haml b/app/views/instructeurs/procedures/download_export.turbo_stream.haml index b841c65a0..c6e47b799 100644 --- a/app/views/instructeurs/procedures/download_export.turbo_stream.haml +++ b/app/views/instructeurs/procedures/download_export.turbo_stream.haml @@ -2,10 +2,10 @@ - if @can_download_dossiers - if @statut.nil? = turbo_stream.update_all '.procedure-actions' do - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path)) - else = turbo_stream.update_all '.dossiers-export' do - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, export_url: method(:download_export_instructeur_procedure_path)) = turbo_stream.update "last-export-alert" do = render partial: "last_export_alert", locals: { export: @last_export, statut: @statut } diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index a067ef265..e3ec7a5a0 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -11,7 +11,7 @@ .procedure-actions - if @can_download_dossiers - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_url: method(:download_export_instructeur_procedure_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), export_url: method(:download_export_instructeur_procedure_path)) .fr-container.flex= render partial: "tabs", locals: { procedure: @procedure, statut: @statut, @@ -72,7 +72,7 @@ - if @dossiers_count > 0 %span.dossiers-export - = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path)) + = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path)) - if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0 = render partial: "dossiers_filter_tags", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut } From 9f715e84d51ae4334665d040befda6a20be3c30a Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 15 Mar 2024 18:20:38 +0100 Subject: [PATCH 23/59] add i18n for export template --- .../export_templates/_form.html.haml | 17 ++++++++++++----- config/locales/models/export_templates/en.yml | 17 +++++++++++++++++ config/locales/models/export_templates/fr.yml | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 config/locales/models/export_templates/en.yml create mode 100644 config/locales/models/export_templates/fr.yml diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 057120f28..b0f7676fd 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -1,9 +1,7 @@ .fr-grid-row.fr-grid-row--gutters .fr-col-12.fr-col-md-8 = form_with url: form_url, model: @export_template, local: true do |f| - = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) do |c| - - c.with_hint do - Indiquez le nom à utiliser pour ce modèle d'export + = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) - if groupe_instructeurs.many? .fr-input-group @@ -19,13 +17,22 @@ = f.hidden_field :kind .fr-input-group{ data: { controller: 'tiptap' } } - = f.label :tiptap_default_dossier_directory, class: "fr-label" + = f.label :tiptap_default_dossier_directory, class: "fr-label" do + = f.object.class.human_attribute_name(:tiptap_default_dossier_directory) + = render EditableChamp::AsteriskMandatoryComponent.new + %span.fr-hint-text + = t('activerecord.attributes.export_template.hints.tiptap_default_dossier_directory') + .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) .fr-input-group{ data: { controller: 'tiptap' } } - = f.label :tiptap_pdf_name, class: "fr-label" + = f.label :tiptap_pdf_name, class: "fr-label" do + = f.object.class.human_attribute_name(:tiptap_pdf_name) + = render EditableChamp::AsteriskMandatoryComponent.new + %span.fr-hint-text + = t('activerecord.attributes.export_template.hints.tiptap_pdf_name') .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) diff --git a/config/locales/models/export_templates/en.yml b/config/locales/models/export_templates/en.yml new file mode 100644 index 000000000..e6cd08856 --- /dev/null +++ b/config/locales/models/export_templates/en.yml @@ -0,0 +1,17 @@ +en: + activerecord: + models: + export_template: Export template + attributes: + export_template: + hints: + name: "The name will be visible by you and the other instructors" + tiptap_default_dossier_directory: "How would you like to name the directory containing the documents of a folder?" + tiptap_pdf_name: "How would you like to name the pdf file containing all the user's answers?" + name: "Template's name" + tiptap_default_dossier_directory: "Directory's name for pdf format" + tiptap_pdf_name: "Export's filename" + errors: + models: + export_template: + dossier_number_mandatory: "must contain dossier's number" diff --git a/config/locales/models/export_templates/fr.yml b/config/locales/models/export_templates/fr.yml new file mode 100644 index 000000000..60852aee0 --- /dev/null +++ b/config/locales/models/export_templates/fr.yml @@ -0,0 +1,17 @@ +fr: + activerecord: + models: + export_template: "Modèle d'export" + attributes: + export_template: + hints: + name: "Le nom sera visible par vous et les autres instructeurs pour générer un export" + tiptap_default_dossier_directory: "Comment souhaitez-vous nommer le répertoire contenant les documents d'un dossier ?" + tiptap_pdf_name: "Comment souhaitez-vous nommer le fichier pdf qui contient toutes les réponses de l'usager ?" + name: "Nom du modèle" + tiptap_default_dossier_directory: Nom du répertoire + tiptap_pdf_name: "Nom du dossier au format pdf" + errors: + models: + export_template: + dossier_number_mandatory: doit contenir le numéro du dossier From 8e8057ddd31cc99eab2e6905b781a40b46b7474d Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 20 Mar 2024 16:02:34 +0100 Subject: [PATCH 24/59] add some specs to export template --- .../tags_substitution_concern_spec.rb | 18 ----- spec/models/export_template_spec.rb | 78 ++++++++++++------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/spec/models/concerns/tags_substitution_concern_spec.rb b/spec/models/concerns/tags_substitution_concern_spec.rb index a11f991f7..e50ecfd70 100644 --- a/spec/models/concerns/tags_substitution_concern_spec.rb +++ b/spec/models/concerns/tags_substitution_concern_spec.rb @@ -604,24 +604,6 @@ describe TagsSubstitutionConcern, type: :model do end end - describe 'some_tags' do - context 'for entreprise procedure' do - let(:for_individual) { false } - it do - tags = template_concern.some_tags - expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_motivation", "dossier_depose_at", "dossier_en_instruction_at", "dossier_processed_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number"] - end - end - - context 'for individual procedure' do - let(:for_individual) { true } - it do - tags = template_concern.some_tags - expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_motivation", "dossier_depose_at", "dossier_en_instruction_at", "dossier_processed_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number"] - end - end - end - describe 'parser' do it do tokens = TagsSubstitutionConcern::TagsParser.parse("hello world --public--, --numéro du dossier--, un test--yolo-- encore du text\n---\n encore du text --- et encore du text\n--tag--") diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index 2aa74744d..cb8d343ed 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -1,7 +1,15 @@ describe ExportTemplate do let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) } - let(:export_template) { build(:export_template, groupe_instructeur:, content:) } - let(:procedure) { create(:procedure_with_dossiers) } + let(:export_template) { create(:export_template, groupe_instructeur:, content:) } + let(:procedure) { create(:procedure_with_dossiers, types_de_champ_public:, for_individual:) } + let(:dossier) { procedure.dossiers.first } + let(:for_individual) { false } + let(:types_de_champ_public) do + [ + { type: :piece_justificative, libelle: "Justificatif de domicile", mandatory: true, stable_id: 3 }, + { type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 5 } + ] + end let(:content) do { "pdf_name" => { @@ -30,13 +38,6 @@ describe ExportTemplate do describe 'new' do let(:export_template) { build(:export_template, groupe_instructeur: groupe_instructeur) } - let(:procedure) { create(:procedure, types_de_champ_public:) } - let(:types_de_champ_public) do - [ - { type: :integer_number, stable_id: 900 }, - { type: :piece_justificative, libelle: "Justificatif de domicile", mandatory: true, stable_id: 910 } - ] - end it 'set default values' do export_template.set_default_values expect(export_template.content).to eq({ @@ -56,7 +57,7 @@ describe ExportTemplate do [ { - "stable_id" => "910", + "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 }] }] } } ] @@ -161,8 +162,18 @@ describe ExportTemplate do end end + describe '#tiptap_convert' do + it 'convert default dossier directory' do + expect(export_template.tiptap_convert(procedure.dossiers.first, "default_dossier_directory")).to eq "DOSSIER_#{dossier.id}" + end + + it 'convert pdf_name' do + expect(export_template.tiptap_convert(procedure.dossiers.first, "pdf_name")).to eq "mon_export_#{dossier.id}" + end + end + describe '#valid?' do - let(:subject) { build(:export_template, content:) } + 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 } @@ -194,7 +205,6 @@ describe ExportTemplate do context 'with valid default dossier directory' do it 'has no error for default_dossier_directory' do expect(subject.valid?).to be_truthy - expect(subject.errors[:default_dossier_directory]).not_to be_present end end @@ -204,23 +214,15 @@ describe ExportTemplate 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 - expect(subject.errors[:default_dossier_directory]).not_to be_present end end - context 'without mention' do - let(:ddd_mention) { { "type" => "mention", "attrs" => {} } } - it "add error for default_dossier_directory" do - expect(subject.valid?).to be_falsey - expect(subject.errors[:default_dossier_directory]).to be_present - end - end - - context 'with mention but without numéro de dossier' do + 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 default_dossier_directory" do + it "add error for tiptap_default_dossier_directory" do expect(subject.valid?).to be_falsey - expect(subject.errors[:default_dossier_directory]).to be_present + 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 @@ -228,7 +230,7 @@ describe ExportTemplate do context 'with valid pdf name' do it 'has no error for pdf name' do expect(subject.valid?).to be_truthy - expect(subject.errors[:pdf_name]).not_to be_present + expect(subject.errors[:tiptap_pdf_name]).not_to be_present end end @@ -247,7 +249,7 @@ describe ExportTemplate do context 'with mention' do it 'has no error for default_dossier_directory' do expect(subject.valid?).to be_truthy - expect(subject.errors[:default_dossier_directory]).not_to be_present + expect(subject.errors[:tiptap_pdf_name]).not_to be_present end end @@ -255,18 +257,18 @@ describe ExportTemplate do let(:pdf_mention) { { "type" => "mention", "attrs" => {} } } it "add error for pdf name" do expect(subject.valid?).to be_falsey - expect(subject.errors[:pdf_name]).to be_present + expect(subject.errors.full_messages).to include "Le champ « Nom de l'export » 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 - expect(subject.errors[:pj_3]).not_to be_present end end @@ -274,9 +276,27 @@ describe ExportTemplate do let(:pj_mention) { { "type" => "mention", "attrs" => {} } } it "add error for pj" do expect(subject.valid?).to be_falsey - expect(subject.errors[:pj_3]).to be_present + expect(subject.errors.full_messages).to include "Le champ « Justificatif de domicile » doit être rempli" end end end end + + describe 'specific_tags' do + context 'for entreprise procedure' do + let(:for_individual) { false } + it do + tags = export_template.specific_tags + expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"] + end + end + + context 'for individual procedure' do + let(:for_individual) { true } + it do + tags = export_template.specific_tags + expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"] + end + end + end end From aeb4bd2ff19b51cf58e83df86118620a701144c6 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 19 Mar 2024 11:03:29 +0100 Subject: [PATCH 25/59] add original-filename tag --- app/models/export_template.rb | 19 +++++++--- .../export_templates/_form.html.haml | 2 +- spec/models/export_template_spec.rb | 35 +++++++++++++------ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 8dc01aa85..b90a81a0e 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -58,16 +58,17 @@ class ExportTemplate < ApplicationRecord end end - def tiptap_convert_pj(dossier, pj_stable_id) - if content_for_pj_id(pj_stable_id)["content"]&.first["content"] - render_attributes_for(content_for_pj_id(pj_stable_id), dossier) + 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) + def render_attributes_for(content_for, dossier, attachment = nil) tiptap = TiptapService.new used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys) substitutions = tags_substitutions(used_tags, dossier, escape: false) + substitutions['original-filename'] = attachment.filename.base if attachment tiptap.to_path(content_for.deep_symbolize_keys, substitutions) end @@ -88,6 +89,14 @@ class ExportTemplate < ApplicationRecord tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten end + def tags_for_pj + specific_tags.push({ + libelle: 'nom original du fichier', + id: 'original-filename', + maybe_null: false + }) + end + private def tiptap_content(key) @@ -134,7 +143,7 @@ class ExportTemplate < ApplicationRecord stable_id = TypeDeChamp.find(type_de_champ_id).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) + suffix(attachment, index, row_index)) + 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 diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index b0f7676fd..9f76d9aee 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -45,7 +45,7 @@ = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } - .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) + .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.tags_for_pj }) .fixed-footer .fr-container diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index cb8d343ed..6d314f136 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -26,12 +26,16 @@ describe ExportTemplate do }, "pjs" => [ - {path: {"type"=>"doc", "content"=>[{"type"=>"paragraph", "content"=>[{"type"=>"mention", "attrs"=>{"id"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" _justif", "type"=>"text"}]}]}, stable_id: "3"}, - { path: - {"type"=>"doc", "content"=>[{"type"=>"paragraph", "content"=>[{"text"=>"cni_", "type"=>"text"}, {"type"=>"mention", "attrs"=>{"id"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" ", "type"=>"text"}]}]}, - stable_id: "5"}, - { path: {"type"=>"doc", "content"=>[{"type"=>"paragraph", "content"=>[{"text"=>"pj_repet_", "type"=>"text"}, {"type"=>"mention", "attrs"=>{"id"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" ", "type"=>"text"}]}]}, - stable_id: "10"} + { 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 @@ -95,9 +99,9 @@ describe ExportTemplate do 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"=>"dossier_number", "label"=>"numéro du dossier"}}, {"text"=>" _justif", "type"=>"text"}]} + "type" => "doc", + "content" => [ + { "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "original-filename", "label" => "nom original du fichier" } }, { "text" => " _justif", "type" => "text" }] } ] }.to_json) end @@ -126,7 +130,7 @@ describe ExportTemplate do dossier.champs_public << champ_pj end it 'returns pj and custom name for pj' do - expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/#{dossier.id}_justif.png"]) + expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif.png"]) end end context 'pj repetable' do @@ -172,6 +176,17 @@ describe ExportTemplate do end end + describe '#tiptap_convert_pj' do + let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) } + let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) } + let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } + + it 'convert pj' do + attachment + expect(export_template.tiptap_convert_pj(dossier, type_de_champ_pj.stable_id, attachment)).to eq "superpj_justif" + end + end + describe '#valid?' do let(:subject) { build(:export_template, groupe_instructeur:, content:) } let(:ddd_text) { "DoSSIER" } From 565f6f44e59e2cd9304727088ef9e1bf4c3525cf Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 20 Mar 2024 16:34:46 +0100 Subject: [PATCH 26/59] make private some methods --- app/models/export_template.rb | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index b90a81a0e..52b983f7f 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -72,19 +72,6 @@ class ExportTemplate < ApplicationRecord tiptap.to_path(content_for.deep_symbolize_keys, substitutions) 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 specific_tags tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten end @@ -117,6 +104,17 @@ class ExportTemplate < ApplicationRecord content_for_stable_id.symbolize_keys.fetch(:path) end + def folder(dossier) + render_attributes_for(content["default_dossier_directory"], dossier) + end + + def export_path(dossier) + File.join(folder(dossier), export_filename(dossier)) + end + + def export_filename(dossier) + "#{render_attributes_for(content["pdf_name"], dossier)}.pdf" + end def path(dossier, attachment, index, row_index) if attachment.name == 'pdf_export_for_instructeur' From 4e1552a9ebdf0c390b3591b0e55fb8cf0085c87f Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 21 Mar 2024 11:39:16 +0100 Subject: [PATCH 27/59] add sample messagerie in preview --- app/views/instructeurs/export_templates/_form.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 9f76d9aee..7ac7704c6 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -71,4 +71,9 @@ %ul %li#preview_pdf_name= @export_template.tiptap_convert(sample_dossier, "pdf_name") - @procedure.pieces_jointes_exportables_list.each do |pj| - %li{id: "preview_pj_#{pj.stable_id}"}= @export_template.tiptap_convert_pj(sample_dossier, pj.stable_id) + %li{ id: "preview_pj_#{pj.stable_id}" }= @export_template.tiptap_convert_pj(sample_dossier, pj.stable_id) + %ul + %li + %span messagerie + %ul + %li un-autre-fichier From 40d7b81e1653e1b353119064574636626931d992 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 26 Mar 2024 10:56:22 +0100 Subject: [PATCH 28/59] add some explanations for export template --- .../export_templates/_form.html.haml | 12 ++++++++++++ .../instructeurs/procedures/exports.html.haml | 19 +++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 7ac7704c6..9be0a9ba0 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -1,6 +1,18 @@ .fr-grid-row.fr-grid-row--gutters .fr-col-12.fr-col-md-8 = form_with url: form_url, model: @export_template, local: true do |f| + #export_template-edit.fr-my-4w{ data: { controller: 'tiptap', tiptap_insert_after_tag_value: ' ' } } + .fr-mb-6w + = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c| + - c.with_body do + Cette page permet d'éditer un modèle d'export et ainsi personnaliser le contenu des exports (pour l'instant, + uniquement au format zip). Ainsi, vous pouvez notamment normaliser le nom des pièces jointes. + Essayez-le et donnez-nous votre avis + en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}. + .fr-highlight + %p.fr-text--sm + N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes et de fichiers. + = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) - if groupe_instructeurs.many? diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml index 67bd7e42c..005eec45f 100644 --- a/app/views/instructeurs/procedures/exports.html.haml +++ b/app/views/instructeurs/procedures/exports.html.haml @@ -23,12 +23,15 @@ - else = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i ) - .fr-table.fr-mt-5w - %table - %caption Liste des modèles d'export - %thead - %tr - %th{ scope: 'col' } Nom du modèle + %h2.fr-mb-1w.fr-mt-8w + Liste des modèles d'export + %p.fr-hint-text + Un modèle d'export permet de personnaliser le nom des fichiers (pour un export au format Zip) + - if @export_templates.any? + .fr-table.fr-table--no-caption.fr-mt-5w + %table + %thead + %tr %th{ scope: 'col' } Nom du modèle %th{ scope: 'col' }= "Groupe instructeur" if @procedure.groupe_instructeurs.many? %tbody @@ -36,10 +39,6 @@ %tr %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many? - - @export_templates.each do |export_template| - %tr - %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) - %td= export_template.groupe_instructeur.label %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 From 213522f23f32a323c1689a5833bc79d5cf8be760 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 29 Mar 2024 13:58:36 +0100 Subject: [PATCH 29/59] remove useless tiptap controller --- app/views/instructeurs/export_templates/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 9be0a9ba0..686e9df1b 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -1,7 +1,7 @@ .fr-grid-row.fr-grid-row--gutters .fr-col-12.fr-col-md-8 = form_with url: form_url, model: @export_template, local: true do |f| - #export_template-edit.fr-my-4w{ data: { controller: 'tiptap', tiptap_insert_after_tag_value: ' ' } } + #export_template-edit.fr-my-4w .fr-mb-6w = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c| - c.with_body do From cec73e07a58378a7394e336b36a5535f76c31417 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Fri, 5 Apr 2024 11:02:17 +0200 Subject: [PATCH 30/59] move preview to be aligned with form --- .../export_templates/_form.html.haml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 686e9df1b..8363e50e2 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -1,17 +1,17 @@ +#export_template-edit.fr-my-4w + .fr-mb-6w + = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c| + - c.with_body do + Cette page permet d'éditer un modèle d'export et ainsi personnaliser le contenu des exports (pour l'instant, + uniquement au format zip). Ainsi, vous pouvez notamment normaliser le nom des pièces jointes. + Essayez-le et donnez-nous votre avis + en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}. + .fr-highlight + %p.fr-text--sm + N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes et de fichiers. .fr-grid-row.fr-grid-row--gutters .fr-col-12.fr-col-md-8 = form_with url: form_url, model: @export_template, local: true do |f| - #export_template-edit.fr-my-4w - .fr-mb-6w - = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c| - - c.with_body do - Cette page permet d'éditer un modèle d'export et ainsi personnaliser le contenu des exports (pour l'instant, - uniquement au format zip). Ainsi, vous pouvez notamment normaliser le nom des pièces jointes. - Essayez-le et donnez-nous votre avis - en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}. - .fr-highlight - %p.fr-text--sm - N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes et de fichiers. = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) From e235131c4cc9f2803c3c9ca037e77f0ecedce711 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 9 Apr 2024 15:12:21 +0200 Subject: [PATCH 31/59] add export template feature flag --- .../export_dropdown_component.html.haml | 1 + .../instructeurs/procedures/exports.html.haml | 37 ++++++++++--------- config/initializers/flipper.rb | 1 + 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml index dbbdbcb07..fbb499483 100644 --- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml +++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml @@ -20,6 +20,7 @@ - menu.with_item do = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do = "Exporter à partir du modèle #{export_template.name}" + - if feature_enabled?(:export_template) - menu.with_item do = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do Ajouter un modèle d'export diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml index 005eec45f..0986a977a 100644 --- a/app/views/instructeurs/procedures/exports.html.haml +++ b/app/views/instructeurs/procedures/exports.html.haml @@ -23,23 +23,24 @@ - else = t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i ) - %h2.fr-mb-1w.fr-mt-8w - Liste des modèles d'export - %p.fr-hint-text - Un modèle d'export permet de personnaliser le nom des fichiers (pour un export au format Zip) - - if @export_templates.any? - .fr-table.fr-table--no-caption.fr-mt-5w - %table - %thead - %tr - %th{ scope: 'col' } Nom du modèle - %th{ scope: 'col' }= "Groupe instructeur" if @procedure.groupe_instructeurs.many? - %tbody - - @export_templates.each do |export_template| + - if feature_enabled?(:export_template) + %h2.fr-mb-1w.fr-mt-8w + Liste des modèles d'export + %p.fr-hint-text + Un modèle d'export permet de personnaliser le nom des fichiers (pour un export au format Zip) + - if @export_templates.any? + .fr-table.fr-table--no-caption.fr-mt-5w + %table + %thead %tr - %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) - %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many? + %th{ scope: 'col' } Nom du modèle + %th{ scope: 'col' }= "Groupe instructeur" if @procedure.groupe_instructeurs.many? + %tbody + - @export_templates.each do |export_template| + %tr + %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) + %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many? - %p - = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do - Ajouter un modèle d'export + %p + = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do + Ajouter un modèle d'export diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index 7eedd85e7..765a98bfe 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -25,6 +25,7 @@ features = [ :dossier_pdf_vide, :engagement_juridique_type_de_champ, :export_order_by_revision, + :export_template, :expression_reguliere_type_de_champ, :gallery_demande, :groupe_instructeur_api_hack, From 6445337be71694ad56b14f604ce579f7516c4682 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 26 Apr 2024 15:36:30 +0200 Subject: [PATCH 32/59] refactor(pj_list): extract pj list in a concern and simplify --- .../concerns/pieces_jointes_list_concern.rb | 39 ++++++++++++++ app/models/procedure.rb | 51 +------------------ app/models/procedure_revision.rb | 1 - .../shared/_procedure_description.html.haml | 30 ++++++----- .../pieces_jointes_list_concern_spec.rb | 50 ++++++++++++++++++ spec/models/procedure_spec.rb | 46 ----------------- .../_procedure_description.html.haml_spec.rb | 2 +- 7 files changed, 107 insertions(+), 112 deletions(-) create mode 100644 app/models/concerns/pieces_jointes_list_concern.rb create mode 100644 spec/models/concerns/pieces_jointes_list_concern_spec.rb diff --git a/app/models/concerns/pieces_jointes_list_concern.rb b/app/models/concerns/pieces_jointes_list_concern.rb new file mode 100644 index 000000000..263b01e9f --- /dev/null +++ b/app/models/concerns/pieces_jointes_list_concern.rb @@ -0,0 +1,39 @@ +module PiecesJointesListConcern + extend ActiveSupport::Concern + + included do + def public_wrapped_partionned_pjs + pieces_jointes_list(public_only: true, wrap_with_parent: true) + .partition { |(pj, _)| pj.condition.nil? } + end + + def pieces_jointes_exportables_list + pieces_jointes_list(exclude_titre_identite: true) + end + + private + + def pieces_jointes_list( + exclude_titre_identite: false, + public_only: false, + wrap_with_parent: false + ) + coordinates = active_revision.revision_types_de_champ + .includes(:type_de_champ, revision_types_de_champ: :type_de_champ) + + coordinates = coordinates.public_only if public_only + + type_champ = ['piece_justificative'] + type_champ << 'titre_identite' if !exclude_titre_identite + + coordinates = coordinates.where(types_de_champ: { type_champ: }) + + return coordinates.map(&:type_de_champ) if !wrap_with_parent + + # we want pj in the form of [[pj1], [pj2, repetition], [pj3, repetition]] + coordinates + .map { |c| c.child? ? [c, c.parent] : [c] } + .map { |a| a.map(&:type_de_champ) } + end + end +end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 02b1a3d15..11cec13d8 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -5,6 +5,7 @@ class Procedure < ApplicationRecord include ProcedureGroupeInstructeurAPIHackConcern include ProcedureSVASVRConcern include ProcedureChorusConcern + include PiecesJointesListConcern include Discard::Model self.discard_column = :hidden_at @@ -982,28 +983,6 @@ class Procedure < ApplicationRecord end end - def pieces_jointes_list? - pieces_jointes_list_without_conditionnal.present? || pieces_jointes_list_with_conditionnal.present? - end - - def pieces_jointes_list_without_conditionnal - pieces_jointes_list do |base_scope| - base_scope.where(types_de_champ: { condition: nil }) - end - end - - def pieces_jointes_exportables_list - pieces_jointes_list(with_private: true, with_titre_identite: false, with_repetition_parent: false) do |base_scope| - base_scope - end.flatten - end - - def pieces_jointes_list_with_conditionnal - pieces_jointes_list do |base_scope| - base_scope.where.not(types_de_champ: { condition: nil }) - end - end - def toggle_routing update!(routing_enabled: self.groupe_instructeurs.active.many?) end @@ -1031,34 +1010,6 @@ class Procedure < ApplicationRecord private - def pieces_jointes_list(with_private: false, with_titre_identite: true, with_repetition_parent: true) - types_de_champ = with_private ? - active_revision.revision_types_de_champ_private_and_public : - active_revision.revision_types_de_champ_public - - type_champs = ['repetition', 'piece_justificative'] - type_champs << 'titre_identite' if with_titre_identite - - scope = yield types_de_champ - .includes(:type_de_champ, revision_types_de_champ: :type_de_champ) - .where(types_de_champ: { type_champ: [type_champs] }) - - scope.each_with_object([]) do |rtdc, list| - if rtdc.type_de_champ.repetition? - rtdc.revision_types_de_champ.each do |rtdc_in_repetition| - if rtdc_in_repetition.type_de_champ.piece_justificative? - to_add = [] - to_add << rtdc_in_repetition.type_de_champ - to_add << rtdc.type_de_champ if with_repetition_parent - list << to_add - end - end - else - list << [rtdc.type_de_champ] - end - end - end - def validate_auto_archive_on_in_the_future return if auto_archive_on.nil? return if auto_archive_on.future? diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index 8326c6533..c8daa1bec 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -8,7 +8,6 @@ class ProcedureRevision < ApplicationRecord has_many :revision_types_de_champ, -> { order(:position, :id) }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_public, -> { root.public_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :revision_types_de_champ_private, -> { root.private_only.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision - has_many :revision_types_de_champ_private_and_public, -> { root.ordered }, class_name: 'ProcedureRevisionTypeDeChamp', foreign_key: :revision_id, dependent: :destroy, inverse_of: :revision has_many :types_de_champ, through: :revision_types_de_champ, source: :type_de_champ has_many :types_de_champ_public, through: :revision_types_de_champ_public, source: :type_de_champ has_many :types_de_champ_private, through: :revision_types_de_champ_private, source: :type_de_champ diff --git a/app/views/shared/_procedure_description.html.haml b/app/views/shared/_procedure_description.html.haml index 922d790c8..e1cce58eb 100644 --- a/app/views/shared/_procedure_description.html.haml +++ b/app/views/shared/_procedure_description.html.haml @@ -48,21 +48,23 @@ #accordion-116.fr-collapse = h render SimpleFormatComponent.new(procedure.description_pj, allow_a: true) - - elsif procedure.pieces_jointes_list? - %section.fr-accordion.pieces_jointes - %h2.fr-accordion__title - %button.fr-accordion__btn{ "aria-controls" => "accordion-116", "aria-expanded" => "false" } - = t('shared.procedure_description.pieces_jointes') - #accordion-116.fr-collapse - - if procedure.pieces_jointes_list_without_conditionnal.present? - %ul - = render partial: "shared/procedure_pieces_jointes_list", collection: procedure.pieces_jointes_list_without_conditionnal, as: :pj + - else + - pj_without_condition, pj_with_condition = procedure.public_wrapped_partionned_pjs + - if pj_without_condition.present? || pj_with_condition.present? + %section.fr-accordion.pieces_jointes + %h2.fr-accordion__title + %button.fr-accordion__btn{ "aria-controls" => "accordion-116", "aria-expanded" => "false" } + = t('shared.procedure_description.pieces_jointes') + #accordion-116.fr-collapse + - if pj_without_condition.present? + %ul + = render partial: "shared/procedure_pieces_jointes_list", collection: pj_without_condition, as: :pj - - if procedure.pieces_jointes_list_with_conditionnal.present? - %h3.fr-text--sm.fr-mb-0.fr-mt-2w - = t('shared.procedure_description.pieces_jointes_conditionnal_list_title') - %ul - = render partial: "shared/procedure_pieces_jointes_list", collection: procedure.pieces_jointes_list_with_conditionnal, as: :pj + - if pj_with_condition.present? + %h3.fr-text--sm.fr-mb-0.fr-mt-2w + = t('shared.procedure_description.pieces_jointes_conditionnal_list_title') + %ul + = render partial: "shared/procedure_pieces_jointes_list", collection: pj_with_condition, as: :pj - estimated_delay_component = Procedure::EstimatedDelayComponent.new(procedure: procedure) - if estimated_delay_component.render? diff --git a/spec/models/concerns/pieces_jointes_list_concern_spec.rb b/spec/models/concerns/pieces_jointes_list_concern_spec.rb new file mode 100644 index 000000000..bc4bdcd47 --- /dev/null +++ b/spec/models/concerns/pieces_jointes_list_concern_spec.rb @@ -0,0 +1,50 @@ +describe PiecesJointesListConcern do + describe '#pieces_jointes_list' do + include Logic + let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) } + let(:types_de_champ_public) do + [ + { type: :integer_number, stable_id: 900 }, + { type: :piece_justificative, libelle: "pj1", stable_id: 910 }, + { type: :piece_justificative, libelle: "pj-cond", stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) }, + { type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "pj2", stable_id: 921 }] }, + { type: :titre_identite, libelle: "pj3", stable_id: 930 } + ] + end + + let(:types_de_champ_private) do + [ + { type: :integer_number, stable_id: 950 }, + { type: :piece_justificative, libelle: "pj5", stable_id: 960 }, + { type: :piece_justificative, libelle: "pj-cond2", stable_id: 961, condition: ds_eq(champ_value(900), constant(1)) }, + { type: :repetition, libelle: "Répétition2", stable_id: 970, children: [{ type: :piece_justificative, libelle: "pj6", stable_id: 971 }] } + ] + end + + let(:types_de_champ) { procedure.active_revision.types_de_champ } + def find_by_stable_id(stable_id) = types_de_champ.find { _1.stable_id == stable_id } + + let(:pj1) { find_by_stable_id(910) } + let(:pjcond) { find_by_stable_id(911) } + let(:repetition) { find_by_stable_id(920) } + let(:pj2) { find_by_stable_id(921) } + let(:pj3) { find_by_stable_id(930) } + + let(:pj5) { find_by_stable_id(960) } + let(:pjcond2) { find_by_stable_id(961) } + let(:repetition2) { find_by_stable_id(970) } + let(:pj6) { find_by_stable_id(971) } + + it "returns the list of pieces jointes without conditional" do + expect(procedure.public_wrapped_partionned_pjs.first).to match_array([[pj1], [pj2, repetition], [pj3]]) + end + + it "returns the list of pieces jointes having conditional" do + expect(procedure.public_wrapped_partionned_pjs.second).to match_array([[pjcond]]) + end + + it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do + expect(procedure.pieces_jointes_exportables_list.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle)) + end + end +end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 8b036cddc..0589985bb 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -1746,52 +1746,6 @@ describe Procedure do end end - describe '#pieces_jointes_list' do - include Logic - let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) } - let(:types_de_champ_public) do - [ - { type: :integer_number, stable_id: 900 }, - { type: :piece_justificative, libelle: "PJ", mandatory: true, stable_id: 910 }, - { type: :piece_justificative, libelle: "PJ-cond", mandatory: true, stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) }, - { type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "PJ2", stable_id: 921 }] }, - { type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 930 } - ] - end - - let(:types_de_champ_private) do - [ - { type: :integer_number, stable_id: 950 }, - { type: :piece_justificative, libelle: "PJ", mandatory: true, stable_id: 960 }, - { type: :piece_justificative, libelle: "PJ-cond", mandatory: true, stable_id: 961, condition: ds_eq(champ_value(900), constant(1)) }, - { type: :repetition, libelle: "Répétition", stable_id: 970, children: [{ type: :piece_justificative, libelle: "PJ2", stable_id: 971 }] } - ] - end - - let(:pj1) { procedure.active_revision.types_de_champ.find { _1.stable_id == 910 } } - let(:pjcond) { procedure.active_revision.types_de_champ.find { _1.stable_id == 911 } } - let(:repetition) { procedure.active_revision.types_de_champ.find { _1.stable_id == 920 } } - let(:pj2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 921 } } - let(:pj3) { procedure.active_revision.types_de_champ.find { _1.stable_id == 930 } } - - let(:pj5) { procedure.active_revision.types_de_champ.find { _1.stable_id == 960 } } - let(:pjcond2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 961 } } - let(:repetition2) { procedure.active_revision.types_de_champ.find { _1.stable_id == 970 } } - let(:pj6) { procedure.active_revision.types_de_champ.find { _1.stable_id == 971 } } - - it "returns the list of pieces jointes without conditional" do - expect(procedure.pieces_jointes_list_without_conditionnal).to match_array([[pj1], [pj2, repetition], [pj3]]) - end - - it "returns the list of pieces jointes having conditional" do - expect(procedure.pieces_jointes_list_with_conditionnal).to match_array([[pjcond]]) - end - - it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do - expect(procedure.pieces_jointes_exportables_list).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6]) - end - end - describe "#attestation_template" do let(:procedure) { create(:procedure) } diff --git a/spec/views/shared/_procedure_description.html.haml_spec.rb b/spec/views/shared/_procedure_description.html.haml_spec.rb index 4bb722895..673584aae 100644 --- a/spec/views/shared/_procedure_description.html.haml_spec.rb +++ b/spec/views/shared/_procedure_description.html.haml_spec.rb @@ -110,7 +110,7 @@ describe 'shared/_procedure_description', type: :view do context 'caching', caching: true do it "works" do - expect(procedure).to receive(:pieces_jointes_list?).once + expect(procedure).to receive(:public_wrapped_partionned_pjs).once 2.times { render partial: 'shared/procedure_description', locals: { procedure: } } end From c8b3b4b45a91bb4e6b01ba9ca74aa8cc9fab498b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 26 Apr 2024 15:04:34 +0200 Subject: [PATCH 33/59] refactor: renaming --- .../instructeurs/export_templates_controller.rb | 2 +- app/models/concerns/pieces_jointes_list_concern.rb | 8 ++++---- app/models/export_template.rb | 2 +- app/validators/export_template_validator.rb | 2 +- app/views/instructeurs/export_templates/_form.html.haml | 2 +- spec/models/concerns/pieces_jointes_list_concern_spec.rb | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb index 9065b2d21..cc41c0108 100644 --- a/app/controllers/instructeurs/export_templates_controller.rb +++ b/app/controllers/instructeurs/export_templates_controller.rb @@ -79,7 +79,7 @@ module Instructeurs end def set_all_pj - @all_pj ||= @procedure.pieces_jointes_exportables_list + @all_pj ||= @procedure.exportables_pieces_jointes end def export_params diff --git a/app/models/concerns/pieces_jointes_list_concern.rb b/app/models/concerns/pieces_jointes_list_concern.rb index 263b01e9f..05ba4b2da 100644 --- a/app/models/concerns/pieces_jointes_list_concern.rb +++ b/app/models/concerns/pieces_jointes_list_concern.rb @@ -3,17 +3,17 @@ module PiecesJointesListConcern included do def public_wrapped_partionned_pjs - pieces_jointes_list(public_only: true, wrap_with_parent: true) + pieces_jointes(public_only: true, wrap_with_parent: true) .partition { |(pj, _)| pj.condition.nil? } end - def pieces_jointes_exportables_list - pieces_jointes_list(exclude_titre_identite: true) + def exportables_pieces_jointes + pieces_jointes(exclude_titre_identite: true) end private - def pieces_jointes_list( + def pieces_jointes( exclude_titre_identite: false, public_only: false, wrap_with_parent: false diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 52b983f7f..136a75999 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -13,7 +13,7 @@ class ExportTemplate < ApplicationRecord content["pdf_name"] = tiptap_json("export_") content["pjs"] = [] - procedure.pieces_jointes_exportables_list.each do |pj| + procedure.exportables_pieces_jointes.each do |pj| content["pjs"] << { "stable_id" => pj.stable_id.to_s, "path" => tiptap_json("#{pj.libelle.parameterize}-") } end end diff --git a/app/validators/export_template_validator.rb b/app/validators/export_template_validator.rb index 1f3475040..2a1158ed6 100644 --- a/app/validators/export_template_validator.rb +++ b/app/validators/export_template_validator.rb @@ -38,7 +38,7 @@ class ExportTemplateValidator < ActiveModel::Validator def validate_pjs(record) record.content["pjs"]&.each do |pj| pj_sym = pj.symbolize_keys - libelle = record.groupe_instructeur.procedure.pieces_jointes_exportables_list.find { _1.stable_id.to_s == pj_sym[:stable_id] }&.libelle&.to_sym + libelle = record.groupe_instructeur.procedure.exportables_pieces_jointes.find { _1.stable_id.to_s == pj_sym[:stable_id] }&.libelle&.to_sym validate_content(record, pj_sym[:path], libelle) end end diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 8363e50e2..18a108ce7 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -82,7 +82,7 @@ %span#preview_default_dossier_directory= @export_template.tiptap_convert(sample_dossier, "default_dossier_directory") %ul %li#preview_pdf_name= @export_template.tiptap_convert(sample_dossier, "pdf_name") - - @procedure.pieces_jointes_exportables_list.each do |pj| + - @procedure.exportables_pieces_jointes.each do |pj| %li{ id: "preview_pj_#{pj.stable_id}" }= @export_template.tiptap_convert_pj(sample_dossier, pj.stable_id) %ul %li diff --git a/spec/models/concerns/pieces_jointes_list_concern_spec.rb b/spec/models/concerns/pieces_jointes_list_concern_spec.rb index bc4bdcd47..0356a52c9 100644 --- a/spec/models/concerns/pieces_jointes_list_concern_spec.rb +++ b/spec/models/concerns/pieces_jointes_list_concern_spec.rb @@ -44,7 +44,7 @@ describe PiecesJointesListConcern do end it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do - expect(procedure.pieces_jointes_exportables_list.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 end From 2dffa9aaa26cc1624c783b73d555f5d95ebfe18d Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 29 Apr 2024 17:09:02 +0200 Subject: [PATCH 34/59] refactor: extract preview --- .../export_templates/_form.html.haml | 18 +----------------- .../export_templates/_preview.html.haml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 app/views/instructeurs/export_templates/_preview.html.haml diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 18a108ce7..ceccdf3f7 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -72,20 +72,4 @@ - sample_dossier = @procedure.dossier_for_preview(current_instructeur) - if sample_dossier .fr-col-12.fr-col-md-4.fr-background-alt--blue-france - .export-template-preview.fr-p-2w.sticky--top - %h2.fr-h4 Aperçu - %ul.tree.fr-text--sm - %li= DownloadableFileService::EXPORT_DIRNAME - %li - %ul - %li - %span#preview_default_dossier_directory= @export_template.tiptap_convert(sample_dossier, "default_dossier_directory") - %ul - %li#preview_pdf_name= @export_template.tiptap_convert(sample_dossier, "pdf_name") - - @procedure.exportables_pieces_jointes.each do |pj| - %li{ id: "preview_pj_#{pj.stable_id}" }= @export_template.tiptap_convert_pj(sample_dossier, pj.stable_id) - %ul - %li - %span messagerie - %ul - %li un-autre-fichier + = render partial: 'preview', locals: { dossier: sample_dossier, export_template: @export_template, procedure: @procedure } diff --git a/app/views/instructeurs/export_templates/_preview.html.haml b/app/views/instructeurs/export_templates/_preview.html.haml new file mode 100644 index 000000000..d12f908ae --- /dev/null +++ b/app/views/instructeurs/export_templates/_preview.html.haml @@ -0,0 +1,17 @@ +#preview.export-template-preview.fr-p-2w.sticky--top + %h2.fr-h4 Aperçu + %ul.tree.fr-text--sm + %li= DownloadableFileService::EXPORT_DIRNAME + %li + %ul + %li + %span#preview_default_dossier_directory= export_template.tiptap_convert(dossier, "default_dossier_directory") + %ul + %li#preview_pdf_name= export_template.tiptap_convert(dossier, "pdf_name") + - procedure.exportables_pieces_jointes.each do |pj| + %li{ id: "preview_pj_#{pj.stable_id}" }= export_template.tiptap_convert_pj(dossier, pj.stable_id) + %ul + %li + %span messagerie + %ul + %li un-autre-fichier From 4a900d812176283ee617fab767d0abb2091fa526 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 30 Apr 2024 09:18:49 +0200 Subject: [PATCH 35/59] refactor(UI): add file extension and number to preview --- .../instructeurs/export_templates/_preview.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/instructeurs/export_templates/_preview.html.haml b/app/views/instructeurs/export_templates/_preview.html.haml index d12f908ae..124abc2c1 100644 --- a/app/views/instructeurs/export_templates/_preview.html.haml +++ b/app/views/instructeurs/export_templates/_preview.html.haml @@ -1,17 +1,17 @@ #preview.export-template-preview.fr-p-2w.sticky--top %h2.fr-h4 Aperçu %ul.tree.fr-text--sm - %li= DownloadableFileService::EXPORT_DIRNAME + %li #{DownloadableFileService::EXPORT_DIRNAME}/ %li %ul %li - %span#preview_default_dossier_directory= export_template.tiptap_convert(dossier, "default_dossier_directory") + %span#preview_default_dossier_directory #{export_template.tiptap_convert(dossier, "default_dossier_directory")}/ %ul - %li#preview_pdf_name= export_template.tiptap_convert(dossier, "pdf_name") + %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) + %li{ id: "preview_pj_#{pj.stable_id}" } #{export_template.tiptap_convert_pj(dossier, pj.stable_id)}-1.jpg %ul %li - %span messagerie + %span messagerie/ %ul - %li un-autre-fichier + %li un-autre-fichier.png From c51792b936683d843c75c15b71dc09fe32dc186e Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 30 Apr 2024 09:34:06 +0200 Subject: [PATCH 36/59] refactor(UI): move extension warning near pjs --- app/views/instructeurs/export_templates/_form.html.haml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index ceccdf3f7..74a29afd5 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -6,9 +6,7 @@ uniquement au format zip). Ainsi, vous pouvez notamment normaliser le nom des pièces jointes. Essayez-le et donnez-nous votre avis en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}. - .fr-highlight - %p.fr-text--sm - N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes et de fichiers. + .fr-grid-row.fr-grid-row--gutters .fr-col-12.fr-col-md-8 = form_with url: form_url, model: @export_template, local: true do |f| @@ -52,6 +50,10 @@ - if @all_pj.any? %h3 Pieces justificatives + .fr-highlight + %p.fr-text--sm + N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes. + - @all_pj.each do |pj| .fr-input-group{ data: { controller: 'tiptap' } } = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" From 1b734aeaedb7c2c656f42dab8c5686d82c96d9eb Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 30 Apr 2024 00:01:45 +0200 Subject: [PATCH 37/59] refactor: simplify preview --- .../instructeurs/export_templates_controller.rb | 12 +++++++----- .../instructeurs/export_templates/_form.html.haml | 11 ++++++----- .../export_templates/preview.turbo_stream.haml | 2 -- 3 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 app/views/instructeurs/export_templates/preview.turbo_stream.haml diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb index cc41c0108..64ef44a4e 100644 --- a/app/controllers/instructeurs/export_templates_controller.rb +++ b/app/controllers/instructeurs/export_templates_controller.rb @@ -46,11 +46,13 @@ module Instructeurs end def preview - param = params.require(:export_template).keys.first - @preview_param = param.delete_prefix("tiptap_") - hash = JSON.parse(params[:export_template][param]).deep_symbolize_keys - export_template = ExportTemplate.new(kind: 'zip', groupe_instructeur: @groupe_instructeurs.first) - @preview_value = export_template.render_attributes_for(hash, @procedure.dossier_for_preview(current_instructeur)) + set_groupe_instructeur + @export_template = @groupe_instructeur.export_templates.build(export_template_params) + @export_template.assign_pj_names(pj_params) + + @sample_dossier = @procedure.dossier_for_preview(current_instructeur) + + render turbo_stream: turbo_stream.replace('preview', partial: 'preview', locals: { export_template: @export_template, procedure: @procedure, dossier: @sample_dossier }) end private diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 74a29afd5..00813d9b6 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -9,7 +9,7 @@ .fr-grid-row.fr-grid-row--gutters .fr-col-12.fr-col-md-8 - = form_with url: form_url, model: @export_template, local: true do |f| + = form_with url: form_url, model: @export_template, local: true, data: { turbo: 'true', controller: 'autosubmit' } do |f| = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) @@ -34,7 +34,7 @@ = t('activerecord.attributes.export_template.hints.tiptap_default_dossier_directory') .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } - = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } + = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input' } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) .fr-input-group{ data: { controller: 'tiptap' } } @@ -44,7 +44,7 @@ %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', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } + = 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? @@ -58,14 +58,15 @@ .fr-input-group{ data: { controller: 'tiptap' } } = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } - = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input', controller: 'turbo-input', turbo_input_url_value: preview_instructeur_export_templates_path } + = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input' } .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.tags_for_pj }) .fixed-footer .fr-container %ul.fr-btns-group.fr-btns-group--inline-md %li - = f.submit "Enregistrer", class: "fr-btn" + %input.hidden{ type: 'submit', formaction: preview_instructeur_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'get' } + = f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' } %li = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary" - if @export_template.persisted? diff --git a/app/views/instructeurs/export_templates/preview.turbo_stream.haml b/app/views/instructeurs/export_templates/preview.turbo_stream.haml deleted file mode 100644 index f6c1e5468..000000000 --- a/app/views/instructeurs/export_templates/preview.turbo_stream.haml +++ /dev/null @@ -1,2 +0,0 @@ -= turbo_stream.update "preview_#{@preview_param}" do - = @preview_value From 6d757db20b2f36c236e1a1cf61e5bdbc86f8ca24 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 30 Apr 2024 19:03:27 +0200 Subject: [PATCH 38/59] fix: champ.row_index and test pjs_for_champs --- app/models/champ.rb | 4 +- .../pieces_justificatives_service_spec.rb | 88 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/app/models/champ.rb b/app/models/champ.rb index 6d3f02f11..a66b91e84 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -96,7 +96,9 @@ class Champ < ApplicationRecord end def row_index - Champ.where(parent:).pluck(:row_id).sort.index(:id) + return nil if parent_id.nil? + + Champ.where(parent_id:).pluck(:row_id).sort.index(row_id) end # used for the `required` html attribute diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 771cb8e96..759a9c91e 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -1,4 +1,92 @@ describe PiecesJustificativesService do + describe 'pjs_for_champs' do + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative }, { type: :repetition, children: [{ type: :piece_justificative }] }]) } + let(:dossier) { create(:dossier, procedure: procedure) } + let(:dossiers) { Dossier.where(id: dossier.id) } + let(:witness) { create(:dossier, procedure: procedure) } + let(:export_template) { double('ExportTemplate') } + let(:pj_service) { PiecesJustificativesService.new(user_profile:, export_template:) } + let(:user_profile) { build(:administrateur) } + + def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp') + def repetition(d) = d.champs.find_by(type: "Champs::RepetitionChamp") + def attachments(champ) = champ.piece_justificative_file.attachments + + before { attach_file_to_champ(pj_champ(witness)) } + + subject { pj_service.send(:pjs_for_champs, dossiers) } + + context 'without any attachment' do + it { expect(subject).to be_empty } + end + + context 'with a single attachment' do + let(:champ) { pj_champ(dossier) } + before { attach_file_to_champ(champ) } + + it do + expect(export_template).to receive(:attachment_and_path) + .with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:) + subject + end + end + + context 'with multiple attachments' do + let(:champ) { pj_champ(dossier) } + + before do + attach_file_to_champ(champ) + attach_file_to_champ(champ) + end + + it do + expect(export_template).to receive(:attachment_and_path) + .with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:) + + expect(export_template).to receive(:attachment_and_path) + .with(dossier, attachments(pj_champ(dossier)).second, index: 1, row_index: nil, champ:) + subject + end + end + + context 'with a repetition' do + let(:first_champ) { repetition(dossier).champs.first } + let(:second_champ) { repetition(dossier).champs.second } + + before do + repetition(dossier).add_row(dossier.revision) + attach_file_to_champ(first_champ) + attach_file_to_champ(first_champ) + + repetition(dossier).add_row(dossier.revision) + attach_file_to_champ(second_champ) + end + + it do + first_child_attachments = attachments(repetition(dossier).champs.first) + second_child_attachments = attachments(repetition(dossier).champs.second) + + expect(export_template).to receive(:attachment_and_path) + .with(dossier, first_child_attachments.first, index: 0, row_index: 0, champ: first_champ) + + expect(export_template).to receive(:attachment_and_path) + .with(dossier, first_child_attachments.second, index: 1, row_index: 0, champ: first_champ) + + expect(export_template).to receive(:attachment_and_path) + .with(dossier, second_child_attachments.first, index: 0, row_index: 1, champ: second_champ) + + count = 0 + + callback = lambda { |*_args| count += 1 } + ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do + subject + end + + expect(count).to eq(18) + end + end + end + describe '.liste_documents' do let(:dossier) { create(:dossier, procedure: procedure) } let(:dossiers) { Dossier.where(id: dossier.id) } From b65686783627afb20eec332b90176d089c8e7b8b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 1 May 2024 21:23:41 +0200 Subject: [PATCH 39/59] refactor(pj_service): do not query for pj_index --- app/services/pieces_justificatives_service.rb | 17 ++++++++++------- .../pieces_justificatives_service_spec.rb | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 22040d4e0..bffbf6ea4 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -158,13 +158,16 @@ class PiecesJustificativesService .includes(:blob) .where(record_type: "Champ", record_id: champ_id_dossier_id.keys) .filter { |a| safe_attachment(a) } - .map do |a, _i| - dossier_id = champ_id_dossier_id[a.record_id] - pj_index = Champ.find(a.record_id).piece_justificative_file.blobs.map(&:id).index(a.blob_id) - if @export_template - @export_template.attachment_and_path(Dossier.find(dossier_id), a, index: pj_index, row_index: a.record.row_index) - else - ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) + .group_by(&:record_id) + .flat_map do |champ_id, attachments| + dossier_id = champ_id_dossier_id[champ_id] + + attachments.map.with_index do |attachment, index| + if @export_template + @export_template.attachment_and_path(Dossier.find(dossier_id), attachment, index: index, row_index: attachment.record.row_index) + else + ActiveStorage::DownloadableFile.pj_and_path(dossier_id, attachment) + end end end end diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 759a9c91e..56917eda7 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -82,7 +82,7 @@ describe PiecesJustificativesService do subject end - expect(count).to eq(18) + expect(count).to eq(10) end end end From 585810553f72827bd17175219fb8b6f491d1426d Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 1 May 2024 21:25:43 +0200 Subject: [PATCH 40/59] refactor(suffix): be consistent with index suffix --- app/models/export_template.rb | 7 ++----- spec/services/procedure_export_service_spec.rb | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 136a75999..4d9bc5f39 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -148,11 +148,8 @@ class ExportTemplate < ApplicationRecord end def suffix(attachment, index, row_index) - suffix = "" - if index >= 1 && !row_index.nil? - suffix += "-#{index + 1}" - suffix += "-#{row_index + 1}" if row_index - end + suffix = "-#{index + 1}" + suffix += "-#{row_index + 1}" if row_index.present? suffix + attachment.filename.extension_with_delimiter end diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index 9109124f4..b463675be 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -544,7 +544,7 @@ describe ProcedureExportService do structure = [ "#{base_fn}/", "#{base_fn}/dossier-#{dossier.id}/", - "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}.txt", + "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-1.txt", "#{base_fn}/dossier-#{dossier.id}/export_#{dossier.id}.pdf" ] expect(files.size).to eq(structure.size) From fe5c655a52c93ab09c5cc0c1b7bd0dec143185a3 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 7 May 2024 09:25:47 +0200 Subject: [PATCH 41/59] spec(perf): count sql queries for 10 dossiers --- .../procedure_export_service_zip_spec.rb | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 spec/services/procedure_export_service_zip_spec.rb diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb new file mode 100644 index 000000000..70ca5fc75 --- /dev/null +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -0,0 +1,87 @@ +describe ProcedureExportService do + let(:instructeur) { create(:instructeur) } + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative }] }]) } + let(:dossiers) { create_list(:dossier, 10, procedure: procedure) } + let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } + let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } + + def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp') + def repetition(d) = d.champs.find_by(type: "Champs::RepetitionChamp") + def attachments(champ) = champ.piece_justificative_file.attachments + + before do + dossiers.each do |dossier| + attach_file_to_champ(pj_champ(dossier)) + + repetition(dossier).add_row(dossier.revision) + attach_file_to_champ(repetition(dossier).champs.first) + attach_file_to_champ(repetition(dossier).champs.first) + + repetition(dossier).add_row(dossier.revision) + attach_file_to_champ(repetition(dossier).champs.second) + end + + allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + end + + describe 'to_zip' do + subject { service.to_zip } + + describe 'generate_dossiers_export' do + context 'with export_template' do + let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) } + + it 'returns a blob with custom filenames' do + VCR.use_cassette('archive/new_file_to_get_200', allow_playback_repeats: true) do + sql_count = 0 + + callback = lambda { |*_args| sql_count += 1 } + ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do + subject + end + + expect(sql_count).to eq(474) + + dossier = dossiers.first + + File.write('tmp.zip', subject.download, mode: 'wb') + File.open('tmp.zip') do |fd| + files = ZipTricks::FileReader.read_zip_structure(io: fd) + base_fn = "export" + structure = [ + "export/", + "export/dossier-#{dossier.id}/", + "export/dossier-#{dossier.id}/export_#{dossier.id}.pdf", + "export/dossier-#{dossier.id}/pj-#{dossier.id}-1.png", + "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-1-1.png", + "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-2-1.png", + "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-1-2.png" + ] + + expect(files.size).to eq(10 * 6 + 1) + expect(structure - files.map(&:filename)).to be_empty + end + FileUtils.remove_entry_secure('tmp.zip') + end + end + end + end + end + + def attach_file_to_champ(champ, safe = true) + attach_file(champ.piece_justificative_file, safe) + end + + def attach_file(attachable, safe = true) + to_be_attached = { + io: StringIO.new("toto"), + filename: "toto.png", content_type: "image/png" + } + + if safe + to_be_attached[:metadata] = { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + end + + attachable.attach(to_be_attached) + end +end From 43fb1ddeb51fad9b609d55a42b3318814de70056 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 14 May 2024 16:37:55 +0200 Subject: [PATCH 42/59] refactor: remove target in tags --- .../concerns/tags_substitution_concern.rb | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 8b0e50b1c..7960e60fb 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -66,7 +66,7 @@ module TagsSubstitutionConcern label: 'numéro du dossier', libelle: 'numéro du dossier', description: '', - target: :id, + lambda: -> (d) { d.id }, available_for_states: Dossier::SOUMIS } @@ -154,21 +154,21 @@ module TagsSubstitutionConcern id: 'individual_gender', libelle: 'civilité', description: 'M., Mme', - target: :gender, + lambda: -> (d) { d.individual&.gender }, available_for_states: Dossier::SOUMIS }, { id: 'individual_last_name', libelle: 'nom', description: "nom de l'usager", - target: :nom, + lambda: -> (d) { d.individual&.nom }, available_for_states: Dossier::SOUMIS }, { id: 'individual_first_name', libelle: 'prénom', description: "prénom de l'usager", - target: :prenom, + lambda: -> (d) { d.individual&.prenom }, available_for_states: Dossier::SOUMIS } ] @@ -178,35 +178,35 @@ module TagsSubstitutionConcern id: 'entreprise_siren', libelle: 'SIREN', description: '', - target: :siren, + lambda: -> (d) { d.etablissement&.entreprise&.siren }, available_for_states: Dossier::SOUMIS }, { id: 'entreprise_numero_tva_intracommunautaire', libelle: 'numéro de TVA intracommunautaire', description: '', - target: :numero_tva_intracommunautaire, + lambda: -> (d) { d.etablissement&.entreprise&.numero_tva_intracommunautaire }, available_for_states: Dossier::SOUMIS }, { id: 'entreprise_siret_siege_social', libelle: 'SIRET du siège social', description: '', - target: :siret_siege_social, + lambda: -> (d) { d.etablissement&.entreprise&.siret_siege_social }, available_for_states: Dossier::SOUMIS }, { id: 'entreprise_raison_sociale', libelle: 'raison sociale', description: '', - target: :raison_sociale, + lambda: -> (d) { d.etablissement&.entreprise&.raison_sociale }, available_for_states: Dossier::SOUMIS }, { id: 'entreprise_adresse', libelle: 'adresse', description: '', - target: :inline_adresse, + lambda: -> (d) { d.etablissement&.entreprise&.inline_adresse }, available_for_states: Dossier::SOUMIS } ] @@ -399,12 +399,8 @@ module TagsSubstitutionConcern end.join('') end - def replace_tag(tag, data) - value = if tag.key?(:target) - data.public_send(tag[:target]) - else - instance_exec(data, &tag[:lambda]) - end + def replace_tag(tag, dossier) + value = instance_exec(dossier, &tag[:lambda]) if escape_unsafe_tags? && tag.fetch(:escapable, true) escape_once(value) @@ -457,8 +453,8 @@ module TagsSubstitutionConcern [champ_private_tags(dossier:), dossier], [dossier_tags, dossier], [ROUTAGE_TAGS, dossier], - [INDIVIDUAL_TAGS, dossier.individual], - [ENTREPRISE_TAGS, dossier.etablissement&.entreprise] + [INDIVIDUAL_TAGS, dossier], + [ENTREPRISE_TAGS, dossier] ] end end From 1c0bd3e0e53ed0e3d37b1c478c8986f220d4f943 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 14 May 2024 16:51:44 +0200 Subject: [PATCH 43/59] refactor: remove unused data --- .../concerns/tags_substitution_concern.rb | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 7960e60fb..a7088fbd5 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -283,20 +283,18 @@ module TagsSubstitutionConcern @escape_unsafe_tags = escape - flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |(tags, data), result| - next if data.nil? - + flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |tags, result| valid_tags = tags_for_dossier_state(tags) valid_tags.each do |tag| - result[tag[:id]] = [tag, data] + result[tag[:id]] = [tag, dossier] end end tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions| substitutions[tag_id] = case flat_tags[tag_id] - in tag, data - replace_tag(tag, data) + in tag, dossier + replace_tag(tag, dossier) else # champ not in dossier, for example during preview on draft revision libelle end @@ -372,8 +370,8 @@ module TagsSubstitutionConcern tokens = parse_tags(text) - tags_and_datas = tags_and_datas_list(dossier).filter_map do |(tags, data)| - data && [tags_for_dossier_state(tags).index_by { _1[:id] }, data] + tags_and_datas = tags_and_datas_list(dossier).filter_map do |tags| + dossier && [tags_for_dossier_state(tags).index_by { _1[:id] }, dossier] end tags_and_datas.reduce(tokens) do |tokens, (tags, data)| @@ -449,12 +447,12 @@ module TagsSubstitutionConcern def tags_and_datas_list(dossier) [ - [champ_public_tags(dossier:), dossier], - [champ_private_tags(dossier:), dossier], - [dossier_tags, dossier], - [ROUTAGE_TAGS, dossier], - [INDIVIDUAL_TAGS, dossier], - [ENTREPRISE_TAGS, dossier] + champ_public_tags(dossier:), + champ_private_tags(dossier:), + dossier_tags, + ROUTAGE_TAGS, + INDIVIDUAL_TAGS, + ENTREPRISE_TAGS ] end end From 3af1cee240b1903b3e9af6207e430ffbdba909ab Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 14 May 2024 17:11:12 +0200 Subject: [PATCH 44/59] refactor: simplify --- app/models/concerns/tags_substitution_concern.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index a7088fbd5..8ddef3b03 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -287,14 +287,13 @@ module TagsSubstitutionConcern valid_tags = tags_for_dossier_state(tags) valid_tags.each do |tag| - result[tag[:id]] = [tag, dossier] + result[tag[:id]] = tag end end tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions| - substitutions[tag_id] = case flat_tags[tag_id] - in tag, dossier - replace_tag(tag, dossier) + substitutions[tag_id] = if flat_tags[tag_id].present? + replace_tag(flat_tags[tag_id], dossier) else # champ not in dossier, for example during preview on draft revision libelle end From e0c867f222906e1c902f5bf6a8cfac60d1d43cf1 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 14 May 2024 17:12:37 +0200 Subject: [PATCH 45/59] refactor: rename --- app/models/concerns/tags_substitution_concern.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 8ddef3b03..bb99cbe5c 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -283,7 +283,7 @@ module TagsSubstitutionConcern @escape_unsafe_tags = escape - flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |tags, result| + flat_tags = available_tags(dossier).each_with_object({}) do |tags, result| valid_tags = tags_for_dossier_state(tags) valid_tags.each do |tag| @@ -369,7 +369,7 @@ module TagsSubstitutionConcern tokens = parse_tags(text) - tags_and_datas = tags_and_datas_list(dossier).filter_map do |tags| + tags_and_datas = available_tags(dossier).filter_map do |tags| dossier && [tags_for_dossier_state(tags).index_by { _1[:id] }, dossier] end @@ -444,7 +444,7 @@ module TagsSubstitutionConcern end end - def tags_and_datas_list(dossier) + def available_tags(dossier) [ champ_public_tags(dossier:), champ_private_tags(dossier:), From d60e7906e040ff3e49a5f04957cf4d2f312b92b8 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 14 May 2024 17:19:52 +0200 Subject: [PATCH 46/59] refactor: memoize flat_tags --- app/models/concerns/tags_substitution_concern.rb | 14 ++++++++------ spec/services/procedure_export_service_zip_spec.rb | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index bb99cbe5c..e68dae815 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -283,17 +283,19 @@ module TagsSubstitutionConcern @escape_unsafe_tags = escape - flat_tags = available_tags(dossier).each_with_object({}) do |tags, result| - valid_tags = tags_for_dossier_state(tags) + if @flat_tags.nil? + @flat_tags = available_tags(dossier).each_with_object({}) do |tags, result| + valid_tags = tags_for_dossier_state(tags) - valid_tags.each do |tag| - result[tag[:id]] = tag + valid_tags.each do |tag| + result[tag[:id]] = tag + end end end tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions| - substitutions[tag_id] = if flat_tags[tag_id].present? - replace_tag(flat_tags[tag_id], dossier) + substitutions[tag_id] = if @flat_tags[tag_id].present? + replace_tag(@flat_tags[tag_id], dossier) else # champ not in dossier, for example during preview on draft revision libelle end diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb index 70ca5fc75..85237c724 100644 --- a/spec/services/procedure_export_service_zip_spec.rb +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -40,7 +40,7 @@ describe ProcedureExportService do subject end - expect(sql_count).to eq(474) + expect(sql_count).to eq(296) dossier = dossiers.first From 420520489df7f91efedf7b1257ec131113b849da Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 15 May 2024 09:30:38 +0200 Subject: [PATCH 47/59] refactor(tags_substitution): simplify --- app/models/concerns/tags_substitution_concern.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index e68dae815..c9b7262af 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -284,13 +284,10 @@ module TagsSubstitutionConcern @escape_unsafe_tags = escape if @flat_tags.nil? - @flat_tags = available_tags(dossier).each_with_object({}) do |tags, result| - valid_tags = tags_for_dossier_state(tags) - - valid_tags.each do |tag| - result[tag[:id]] = tag - end - end + @flat_tags = available_tags(dossier) + .flatten + .then { tags_for_dossier_state(_1) } + .index_by { _1[:id] } end tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions| From e8a175d3102af18cfb91250759f05cf8ec8cc3ea Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 15 May 2024 10:30:51 +0200 Subject: [PATCH 48/59] refactor: be explicite with memoization --- app/models/concerns/tags_substitution_concern.rb | 14 +++++++++----- app/models/export_template.rb | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index c9b7262af..373ad3018 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -275,7 +275,7 @@ module TagsSubstitutionConcern used_tags_and_libelle_for(text).map { _1.first.nil? ? _1.second : _1.first } end - def tags_substitutions(tags_and_libelles, dossier, escape: true) + def tags_substitutions(tags_and_libelles, dossier, escape: true, memoize: false) # NOTE: # - tags_and_libelles est un simple Set de couples (tag_id, libelle) (pas la même structure que dans replace_tags) # - dans `replace_tags`, on fait référence à des tags avec ou sans id, mais pas ici, @@ -283,16 +283,20 @@ module TagsSubstitutionConcern @escape_unsafe_tags = escape - if @flat_tags.nil? - @flat_tags = available_tags(dossier) + flat_tags = if memoize && @flat_tags.present? + @flat_tags + else + available_tags(dossier) .flatten .then { tags_for_dossier_state(_1) } .index_by { _1[:id] } end + @flat_tags = flat_tags if memoize + tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions| - substitutions[tag_id] = if @flat_tags[tag_id].present? - replace_tag(@flat_tags[tag_id], dossier) + substitutions[tag_id] = if flat_tags[tag_id].present? + replace_tag(flat_tags[tag_id], dossier) else # champ not in dossier, for example during preview on draft revision libelle end diff --git a/app/models/export_template.rb b/app/models/export_template.rb index 4d9bc5f39..dfbb57f3f 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -67,7 +67,7 @@ class ExportTemplate < ApplicationRecord def render_attributes_for(content_for, dossier, attachment = nil) tiptap = TiptapService.new used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys) - substitutions = tags_substitutions(used_tags, dossier, escape: false) + substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true) substitutions['original-filename'] = attachment.filename.base if attachment tiptap.to_path(content_for.deep_symbolize_keys, substitutions) end From 080bcd8628a8330443111163213d3e9b3466a81a Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 15 May 2024 10:49:27 +0200 Subject: [PATCH 49/59] refactor: DossierPreloader rename includes_for_dossier -> includes_for_champ --- app/lib/recovery/exporter.rb | 2 +- app/models/dossier_preloader.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/lib/recovery/exporter.rb b/app/lib/recovery/exporter.rb index f414df28a..3b681608b 100644 --- a/app/lib/recovery/exporter.rb +++ b/app/lib/recovery/exporter.rb @@ -18,7 +18,7 @@ module Recovery etablissement: :exercices, revision: :procedure) @dossiers = DossierPreloader.new(dossier_with_data, - includes_for_dossier: [:geo_areas, etablissement: :exercices], + includes_for_champ: [:geo_areas, etablissement: :exercices], includes_for_etablissement: [:exercices]).all @file_path = file_path end diff --git a/app/models/dossier_preloader.rb b/app/models/dossier_preloader.rb index 2f541ad93..28cb4b769 100644 --- a/app/models/dossier_preloader.rb +++ b/app/models/dossier_preloader.rb @@ -1,10 +1,10 @@ class DossierPreloader DEFAULT_BATCH_SIZE = 2000 - def initialize(dossiers, includes_for_dossier: [], includes_for_etablissement: []) + def initialize(dossiers, includes_for_champ: [], includes_for_etablissement: []) @dossiers = dossiers @includes_for_etablissement = includes_for_etablissement - @includes_for_dossier = includes_for_dossier + @includes_for_champ = includes_for_champ end def in_batches(size = DEFAULT_BATCH_SIZE) @@ -37,7 +37,7 @@ class DossierPreloader end def load_dossiers(dossiers, pj_template: false) - to_include = @includes_for_dossier.dup + to_include = @includes_for_champ.dup to_include << [piece_justificative_file_attachments: :blob] if pj_template From fa7d5cfa33fee57675e4a16f036e374dd6955581 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 15 May 2024 15:06:38 +0200 Subject: [PATCH 50/59] fix test --- spec/models/export_template_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index 6d314f136..0316b8edf 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -130,7 +130,7 @@ describe ExportTemplate do dossier.champs_public << champ_pj end it 'returns pj and custom name for pj' do - expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif.png"]) + expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif-1.png"]) end end context 'pj repetable' do @@ -161,7 +161,7 @@ describe ExportTemplate do dossier.champs_public << champ_pj end it 'rename repetable pj' do - expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/pj_repet_#{dossier.id}.png"]) + expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/pj_repet_#{dossier.id}-1.png"]) end end end From 9effa9e030623a5ad1da20e6192b2118a56ada75 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 17 May 2024 16:47:30 +0200 Subject: [PATCH 51/59] perf(zip): preload_dossier earlier --- app/lib/active_storage/downloadable_file.rb | 6 +- app/services/pieces_justificatives_service.rb | 60 +++++++------------ 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb index 57e919ff3..15b796aa5 100644 --- a/app/lib/active_storage/downloadable_file.rb +++ b/app/lib/active_storage/downloadable_file.rb @@ -4,7 +4,11 @@ class ActiveStorage::DownloadableFile def self.create_list_from_dossiers(dossiers:, user_profile:, export_template: nil) pj_service = PiecesJustificativesService.new(user_profile:, export_template:) - pj_service.generate_dossiers_export(dossiers) + pj_service.liste_documents(dossiers) + dossiers = dossiers + .includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert], revision: [:revision_types_de_champ, :types_de_champ_public, :types_de_champ_private]) + + loaded_dossiers = DossierPreloader.new(dossiers).in_batches + pj_service.generate_dossiers_export(loaded_dossiers) + pj_service.liste_documents(loaded_dossiers) end def self.cleanup_list_from_dossier(files) diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index bffbf6ea4..4b83d3b28 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -7,22 +7,18 @@ class PiecesJustificativesService def liste_documents(dossiers) bill_ids = [] - docs = dossiers.in_batches.flat_map do |batch| - pjs = pjs_for_champs(batch) + - pjs_for_commentaires(batch) + - pjs_for_dossier(batch) + - pjs_for_avis(batch) + docs = pjs_for_champs(dossiers) + + pjs_for_commentaires(dossiers) + + pjs_for_dossier(dossiers) + + pjs_for_avis(dossiers) - if liste_documents_allows?(:with_bills) - # some bills are shared among operations - # so first, all the bill_ids are fetched - operation_logs, some_bill_ids = operation_logs_and_signature_ids(batch) + if liste_documents_allows?(:with_bills) + # some bills are shared among operations + # so first, all the bill_ids are fetched + operation_logs, some_bill_ids = operation_logs_and_signature_ids(dossiers) - pjs += operation_logs - bill_ids += some_bill_ids - end - - pjs + docs += operation_logs + bill_ids += some_bill_ids end if liste_documents_allows?(:with_bills) @@ -33,14 +29,12 @@ class PiecesJustificativesService docs end - def generate_dossiers_export(dossiers) + def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s return [] if dossiers.empty? pdfs = [] procedure = dossiers.first.procedure - dossiers = dossiers.includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert]) - dossiers = DossierPreloader.new(dossiers).in_batches dossiers.each do |dossier| dossier.association(:procedure).target = procedure @@ -50,7 +44,6 @@ class PiecesJustificativesService acls: acl_for_dossier_export(procedure), dossier: dossier }) - a = ActiveStorage::FakeAttachment.new( file: StringIO.new(pdf), filename: "export-#{dossier.id}.pdf", @@ -142,34 +135,21 @@ class PiecesJustificativesService end def pjs_for_champs(dossiers) - champs = Champ - .joins(:piece_justificative_file_attachments) - .where(type: "Champs::PieceJustificativeChamp", dossier: dossiers) + champs = dossiers.flat_map(&:champs).filter { _1.type == "Champs::PieceJustificativeChamp" } if !liste_documents_allows?(:with_champs_private) - champs = champs.where(private: false) + champs = champs.reject(&:private?) end - champ_id_dossier_id = champs - .pluck(:id, :dossier_id) - .to_h - - ActiveStorage::Attachment - .includes(:blob) - .where(record_type: "Champ", record_id: champ_id_dossier_id.keys) - .filter { |a| safe_attachment(a) } - .group_by(&:record_id) - .flat_map do |champ_id, attachments| - dossier_id = champ_id_dossier_id[champ_id] - - attachments.map.with_index do |attachment, index| - if @export_template - @export_template.attachment_and_path(Dossier.find(dossier_id), attachment, index: index, row_index: attachment.record.row_index) - else - ActiveStorage::DownloadableFile.pj_and_path(dossier_id, attachment) - end + champs.flat_map do |champ| + champ.piece_justificative_file_attachments.map.with_index do |attachment, index| + if @export_template + @export_template.attachment_and_path(champ.dossier, attachment, index:, row_index: champ.row_index, champ:) + else + ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment) end end + end end def pjs_for_commentaires(dossiers) From a7e29c4ea63495a7a35a133ac6b540252ced151f Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 17 May 2024 17:00:51 +0200 Subject: [PATCH 52/59] perf(spec): new count ! --- spec/services/procedure_export_service_zip_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb index 85237c724..71362ec1e 100644 --- a/spec/services/procedure_export_service_zip_spec.rb +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -1,7 +1,7 @@ describe ProcedureExportService do let(:instructeur) { create(:instructeur) } let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative }] }]) } - let(:dossiers) { create_list(:dossier, 10, procedure: procedure) } + let(:dossiers) { create_list(:dossier, 100, procedure: procedure) } let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } @@ -40,7 +40,7 @@ describe ProcedureExportService do subject end - expect(sql_count).to eq(296) + expect(sql_count).to eq(272) dossier = dossiers.first From ca12a56e6aba76fe32bf4cadfce340896d5c047e Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 17 May 2024 17:01:41 +0200 Subject: [PATCH 53/59] perf(zip): give champ to avoid seeking stable_id --- app/models/export_template.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index dfbb57f3f..f5ed164b9 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -45,10 +45,10 @@ class ExportTemplate < ApplicationRecord end end - def attachment_and_path(dossier, attachment, index: 0, row_index: nil) + def attachment_and_path(dossier, attachment, index: 0, row_index: nil, champ: nil) [ attachment, - path(dossier, attachment, index, row_index) + path(dossier, attachment, index, row_index, champ) ] end @@ -116,7 +116,7 @@ class ExportTemplate < ApplicationRecord "#{render_attributes_for(content["pdf_name"], dossier)}.pdf" end - def path(dossier, attachment, index, row_index) + def path(dossier, attachment, index, row_index, champ) if attachment.name == 'pdf_export_for_instructeur' return export_path(dossier) end @@ -130,15 +130,14 @@ class ExportTemplate < ApplicationRecord 'avis' else # for attachment - return attachment_path(dossier, attachment, index, row_index) + 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) - type_de_champ_id = dossier.champs.find(attachment.record_id).type_de_champ_id - stable_id = TypeDeChamp.find(type_de_champ_id).stable_id + 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)) From 6184b33a18efb7d1e53456dd7aa54457d9f333c6 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 17 May 2024 17:36:40 +0200 Subject: [PATCH 54/59] perf(preloader): preloader use batch for batches --- app/lib/active_storage/downloadable_file.rb | 9 +++++---- app/models/dossier_preloader.rb | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/lib/active_storage/downloadable_file.rb b/app/lib/active_storage/downloadable_file.rb index 15b796aa5..e42c19b54 100644 --- a/app/lib/active_storage/downloadable_file.rb +++ b/app/lib/active_storage/downloadable_file.rb @@ -4,11 +4,12 @@ class ActiveStorage::DownloadableFile def self.create_list_from_dossiers(dossiers:, user_profile:, export_template: nil) pj_service = PiecesJustificativesService.new(user_profile:, export_template:) - dossiers = dossiers - .includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert], revision: [:revision_types_de_champ, :types_de_champ_public, :types_de_champ_private]) + files = [] + DossierPreloader.new(dossiers).in_batches_with_block do |loaded_dossiers| + files += pj_service.generate_dossiers_export(loaded_dossiers) + pj_service.liste_documents(loaded_dossiers) + end - loaded_dossiers = DossierPreloader.new(dossiers).in_batches - pj_service.generate_dossiers_export(loaded_dossiers) + pj_service.liste_documents(loaded_dossiers) + files end def self.cleanup_list_from_dossier(files) diff --git a/app/models/dossier_preloader.rb b/app/models/dossier_preloader.rb index 28cb4b769..c1a3e614e 100644 --- a/app/models/dossier_preloader.rb +++ b/app/models/dossier_preloader.rb @@ -13,6 +13,16 @@ class DossierPreloader dossiers end + def in_batches_with_block(size = DEFAULT_BATCH_SIZE, &block) + @dossiers.in_batches(of: size) do |batch| + data = Dossier.where(id: batch.ids).includes(:individual, :traitement, :etablissement, user: :france_connect_informations, avis: :expert, commentaires: [:instructeur, :expert], revision: :revision_types_de_champ) + + dossiers = data.to_a + load_dossiers(dossiers) + yield(dossiers) + end + end + def all(pj_template: false) dossiers = @dossiers.to_a load_dossiers(dossiers, pj_template:) From 8628ec162190d65fd715b9fbc94401e80affc937 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 17 May 2024 17:38:54 +0200 Subject: [PATCH 55/59] perf(spec): new record ! Co-authored-by: Christophe Robillard --- spec/services/procedure_export_service_zip_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb index 71362ec1e..1baace907 100644 --- a/spec/services/procedure_export_service_zip_spec.rb +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -1,7 +1,7 @@ describe ProcedureExportService do let(:instructeur) { create(:instructeur) } let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative }] }]) } - let(:dossiers) { create_list(:dossier, 100, procedure: procedure) } + let(:dossiers) { create_list(:dossier, 10, procedure: procedure) } let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } @@ -40,7 +40,7 @@ describe ProcedureExportService do subject end - expect(sql_count).to eq(272) + expect(sql_count).to eq(89) dossier = dossiers.first From e38999efda23ca135c8a2bddbcd9844b75f1a4ae Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Sat, 18 May 2024 10:02:29 +0200 Subject: [PATCH 56/59] perf(pj service): compute row_id without extraneous requests --- app/models/champ.rb | 6 ------ app/services/pieces_justificatives_service.rb | 12 ++++++++++-- spec/services/procedure_export_service_zip_spec.rb | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/models/champ.rb b/app/models/champ.rb index a66b91e84..74b5fd1e9 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -95,12 +95,6 @@ class Champ < ApplicationRecord [row_id, stable_id].compact end - def row_index - return nil if parent_id.nil? - - Champ.where(parent_id:).pluck(:row_id).sort.index(row_id) - end - # used for the `required` html attribute # check visibility to avoid hidden required input # which prevent the form from being sent. diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 4b83d3b28..612adba35 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -141,10 +141,18 @@ class PiecesJustificativesService champs = champs.reject(&:private?) end + champs_id_row_index = champs.filter { _1.row_id.present? }.group_by(&:dossier_id).values.each_with_object({}) do |champs_for_dossier, hash| + champs_for_dossier.group_by(&:stable_id).values.each do |champs_for_stable_id| + champs_for_stable_id.sort_by(&:row_id).each.with_index { |c, index| hash[c.id] = index } + end + end + champs.flat_map do |champ| - champ.piece_justificative_file_attachments.map.with_index do |attachment, index| + champ.piece_justificative_file_attachments.filter { |a| safe_attachment(a) }.map.with_index do |attachment, index| + row_index = champs_id_row_index[champ.id] + if @export_template - @export_template.attachment_and_path(champ.dossier, attachment, index:, row_index: champ.row_index, champ:) + @export_template.attachment_and_path(champ.dossier, attachment, index:, row_index:, champ:) else ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment) end diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb index 1baace907..e50cce89f 100644 --- a/spec/services/procedure_export_service_zip_spec.rb +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -40,7 +40,7 @@ describe ProcedureExportService do subject end - expect(sql_count).to eq(89) + expect(sql_count).to eq(58) dossier = dossiers.first @@ -58,7 +58,7 @@ describe ProcedureExportService do "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-1-2.png" ] - expect(files.size).to eq(10 * 6 + 1) + expect(files.size).to eq(dossiers.count * 6 + 1) expect(structure - files.map(&:filename)).to be_empty end FileUtils.remove_entry_secure('tmp.zip') From d3b700326deb2ced2e0a1dbcc6d8f1e39a2ae9db Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 21 May 2024 17:05:34 +0200 Subject: [PATCH 57/59] spec: fix --- spec/models/export_template_spec.rb | 6 +++--- spec/services/procedure_export_service_zip_spec.rb | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index 0316b8edf..f0602597a 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -130,7 +130,7 @@ describe ExportTemplate do dossier.champs_public << champ_pj end it 'returns pj and custom name for pj' do - expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif-1.png"]) + expect(export_template.attachment_and_path(dossier, attachment, champ: champ_pj)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif-1.png"]) end end context 'pj repetable' do @@ -161,7 +161,7 @@ describe ExportTemplate do dossier.champs_public << champ_pj end it 'rename repetable pj' do - expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/pj_repet_#{dossier.id}-1.png"]) + 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 @@ -272,7 +272,7 @@ describe ExportTemplate 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 de l'export » doit être rempli" + expect(subject.errors.full_messages).to include "Le champ « Nom du dossier au format pdf » doit être rempli" end end end diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb index e50cce89f..0daced35e 100644 --- a/spec/services/procedure_export_service_zip_spec.rb +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -1,6 +1,6 @@ describe ProcedureExportService do let(:instructeur) { create(:instructeur) } - let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative }] }]) } + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative, libelle: 'repet_pj' }] }]) } let(:dossiers) { create_list(:dossier, 10, procedure: procedure) } let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) } let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } @@ -40,22 +40,21 @@ describe ProcedureExportService do subject end - expect(sql_count).to eq(58) + expect(sql_count <= 58).to be_truthy dossier = dossiers.first File.write('tmp.zip', subject.download, mode: 'wb') File.open('tmp.zip') do |fd| files = ZipTricks::FileReader.read_zip_structure(io: fd) - base_fn = "export" structure = [ "export/", "export/dossier-#{dossier.id}/", "export/dossier-#{dossier.id}/export_#{dossier.id}.pdf", "export/dossier-#{dossier.id}/pj-#{dossier.id}-1.png", - "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-1-1.png", - "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-2-1.png", - "export/dossier-#{dossier.id}/libelle-du-champ-2-#{dossier.id}-1-2.png" + "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-1-1.png", + "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-2-1.png", + "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-1-2.png" ] expect(files.size).to eq(dossiers.count * 6 + 1) From 4fb03e3967d203743498f75940c8c05296fd732e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 22 May 2024 12:23:38 +0200 Subject: [PATCH 58/59] fix: remove useless code --- app/lib/download_manager/parallel_download_queue.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/lib/download_manager/parallel_download_queue.rb b/app/lib/download_manager/parallel_download_queue.rb index 35a6b8695..16dcbd762 100644 --- a/app/lib/download_manager/parallel_download_queue.rb +++ b/app/lib/download_manager/parallel_download_queue.rb @@ -12,8 +12,6 @@ module DownloadManager end def download_all - # TODO: arriver à enelver ce parametrage d'ActiveStorage - ActiveStorage::Current.url_options = { host: ENV.fetch("APP_HOST") } hydra = Typhoeus::Hydra.new(max_concurrency: DOWNLOAD_MAX_PARALLEL) attachments.each do |attachment, path| From 0869168bd33160e221316988d739938809f41901 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Thu, 23 May 2024 09:59:09 +0200 Subject: [PATCH 59/59] spec: test champs_id_row_index --- app/services/pieces_justificatives_service.rb | 28 +++++++-- .../pieces_justificatives_service_spec.rb | 61 +++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 612adba35..0fdbc2be9 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -141,11 +141,7 @@ class PiecesJustificativesService champs = champs.reject(&:private?) end - champs_id_row_index = champs.filter { _1.row_id.present? }.group_by(&:dossier_id).values.each_with_object({}) do |champs_for_dossier, hash| - champs_for_dossier.group_by(&:stable_id).values.each do |champs_for_stable_id| - champs_for_stable_id.sort_by(&:row_id).each.with_index { |c, index| hash[c.id] = index } - end - end + champs_id_row_index = compute_champ_id_row_index(champs) champs.flat_map do |champ| champ.piece_justificative_file_attachments.filter { |a| safe_attachment(a) }.map.with_index do |attachment, index| @@ -301,4 +297,26 @@ class PiecesJustificativesService .blob .virus_scan_result == ActiveStorage::VirusScanner::SAFE end + + # given + # repet_0 (stable_id: r0) + # # row_0 + # # # pj_champ_0 (stable_id: 0) + # # row_1 + # # # pj_champ_1 (stable_id: 0) + # repet_1 (stable_id: r1) + # # row_0 + # # # pj_champ_2 (stable_id: 1) + # # # pj_champ_3 (stable_id: 2) + # # row_1 + # # # pj_champ_4 (stable_id: 1) + # # # pj_champ_5 (stable_id: 2) + # it returns { pj_0.id => 0, pj_1.id => 1, pj_2.id => 0, pj_3.id => 0, pj_4.id => 1, pj_5.id => 1 } + def compute_champ_id_row_index(champs) + champs.filter(&:child?).group_by(&:dossier_id).values.each_with_object({}) do |children_for_dossier, hash| + children_for_dossier.group_by(&:stable_id).values.each do |champs_for_stable_id| + champs_for_stable_id.sort_by(&:row_id).each.with_index { |c, index| hash[c.id] = index } + end + end + end end diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 56917eda7..cef564981 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -417,6 +417,67 @@ describe PiecesJustificativesService do end end + describe '#compute_champ_id_row_index' do + let(:user_profile) { build(:administrateur) } + let(:types_de_champ_public) do + [ + { type: :repetition, children: [{ type: :piece_justificative }] }, + { type: :repetition, children: [{ type: :piece_justificative }, { type: :piece_justificative }] } + ] + end + + let(:procedure) { create(:procedure, types_de_champ_public:) } + let(:dossier_1) { create(:dossier, procedure:) } + let(:champs) { dossier_1.champs } + + def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp') + def repetition(d, index:) = d.champs_public.filter(&:repetition?)[index] + + subject { PiecesJustificativesService.new(user_profile:, export_template: nil).send(:compute_champ_id_row_index, champs) } + + before do + pj_champ(dossier_1) + + # repet_0 (stable_id: r0) + # # row_0 + # # # pj_champ_0 (stable_id: 0) + # # row_1 + # # # pj_champ_1 (stable_id: 0) + # repet_1 (stable_id: r1) + # # row_0 + # # # pj_champ_2 (stable_id: 1) + # # # pj_champ_3 (stable_id: 2) + # # row_1 + # # # pj_champ_4 (stable_id: 1) + # # # pj_champ_5 (stable_id: 2) + + repet_0 = repetition(dossier_1, index: 0) + repet_1 = repetition(dossier_1, index: 1) + + repet_0.add_row(dossier_1.revision) + repet_0.add_row(dossier_1.revision) + + repet_1.add_row(dossier_1.revision) + repet_1.add_row(dossier_1.revision) + end + + it do + champs = dossier_1.champs_public + repet_0 = champs[0] + pj_0 = repet_0.rows.first.first + pj_1 = repet_0.rows.second.first + + repet_1 = champs[1] + pj_2 = repet_1.rows.first.first + pj_3 = repet_1.rows.first.second + + pj_4 = repet_1.rows.second.first + pj_5 = repet_1.rows.second.second + + is_expected.to eq({ pj_0.id => 0, pj_1.id => 1, pj_2.id => 0, pj_3.id => 0, pj_4.id => 1, pj_5.id => 1 }) + end + end + def attach_file_to_champ(champ, safe = true) attach_file(champ.piece_justificative_file, safe) end