From 53a48f963d1f56164d1666a4da6e5a130fb6e50c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 22 Jul 2024 17:01:32 +0200 Subject: [PATCH 01/16] tdc: add libelle_as_filename --- app/models/type_de_champ.rb | 6 ++++++ spec/models/type_de_champ_spec.rb | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index bcf24c0f1..4033b0f09 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -676,6 +676,12 @@ class TypeDeChamp < ApplicationRecord end end + def libelle_as_filename + libelle.gsub(/[[:space:]]+/, ' ') + .truncate(30, omission: '', separator: ' ') + .parameterize + end + class << self def champ_value(type_champ, champ) dynamic_type_class = type_champ_to_class_name(type_champ).constantize diff --git a/spec/models/type_de_champ_spec.rb b/spec/models/type_de_champ_spec.rb index 95cd2bb44..8cacac706 100644 --- a/spec/models/type_de_champ_spec.rb +++ b/spec/models/type_de_champ_spec.rb @@ -302,4 +302,12 @@ describe TypeDeChamp do it { expect(create(:type_de_champ, :header_section, libelle: " 2.3 Test").libelle).to eq("2.3 Test") } it { expect(create(:type_de_champ, libelle: " fix me ").libelle).to eq("fix me") } end + + describe '#safe_filename' do + subject { build(:type_de_champ, libelle:).libelle_as_filename } + + let(:libelle) { " #/🐉 1 très intéressant Bilan " } + + it { is_expected.to eq("1-tres-interessant-bilan") } + end end From 106698a2421b8d6efe6bd8a145e8360a8c46c666 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Thu, 18 Jul 2024 09:55:06 +0200 Subject: [PATCH 02/16] add dedicated dossier_folder, export_pdf, pjs columns to export model Destroy all previous incompatible ExportTemplate --- ...dd_dossier_folder_column_to_export_template.rb | 15 +++++++++++++++ db/schema.rb | 3 +++ 2 files changed, 18 insertions(+) create mode 100644 db/migrate/20240713090744_add_dossier_folder_column_to_export_template.rb diff --git a/db/migrate/20240713090744_add_dossier_folder_column_to_export_template.rb b/db/migrate/20240713090744_add_dossier_folder_column_to_export_template.rb new file mode 100644 index 000000000..69a31e4b6 --- /dev/null +++ b/db/migrate/20240713090744_add_dossier_folder_column_to_export_template.rb @@ -0,0 +1,15 @@ +class AddDossierFolderColumnToExportTemplate < ActiveRecord::Migration[7.0] + def up + execute "DELETE FROM export_templates;" + + add_column :export_templates, :dossier_folder, :jsonb, default: nil, null: false + add_column :export_templates, :export_pdf, :jsonb, default: nil, null: false + add_column :export_templates, :pjs, :jsonb, array: true, default: [], null: false + end + + def down + remove_column :export_templates, :dossier_folder + remove_column :export_templates, :export_pdf + remove_column :export_templates, :pjs + end +end diff --git a/db/schema.rb b/db/schema.rb index 9239db004..0544b546c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -599,9 +599,12 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_16_091043) do create_table "export_templates", force: :cascade do |t| t.jsonb "content", default: {} t.datetime "created_at", null: false + t.jsonb "dossier_folder", null: false + t.jsonb "export_pdf", null: false t.bigint "groupe_instructeur_id", null: false t.string "kind", null: false t.string "name", null: false + t.jsonb "pjs", default: [], null: false, array: true t.datetime "updated_at", null: false t.index ["groupe_instructeur_id"], name: "index_export_templates_on_groupe_instructeur_id" end From 24109a01280bc68a14b3721b01d1e20a4913aa69 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 26 Jul 2024 10:06:38 +0200 Subject: [PATCH 03/16] add exportables_pieces_jointes_for_all_versions and outdated to pj_list_concern --- .../concerns/pieces_jointes_list_concern.rb | 16 ++- .../pieces_jointes_list_concern_spec.rb | 102 +++++++++++------- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/app/models/concerns/pieces_jointes_list_concern.rb b/app/models/concerns/pieces_jointes_list_concern.rb index 05ba4b2da..222afa773 100644 --- a/app/models/concerns/pieces_jointes_list_concern.rb +++ b/app/models/concerns/pieces_jointes_list_concern.rb @@ -11,14 +11,26 @@ module PiecesJointesListConcern pieces_jointes(exclude_titre_identite: true) end + def exportables_pieces_jointes_for_all_versions + pieces_jointes( + exclude_titre_identite: true, + revision: revisions + ).sort_by { - _1.id }.uniq(&:stable_id) + end + + def outdated_exportables_pieces_jointes + exportables_pieces_jointes_for_all_versions - exportables_pieces_jointes + end + private def pieces_jointes( exclude_titre_identite: false, public_only: false, - wrap_with_parent: false + wrap_with_parent: false, + revision: active_revision ) - coordinates = active_revision.revision_types_de_champ + coordinates = ProcedureRevisionTypeDeChamp.where(revision:) .includes(:type_de_champ, revision_types_de_champ: :type_de_champ) coordinates = coordinates.public_only if public_only diff --git a/spec/models/concerns/pieces_jointes_list_concern_spec.rb b/spec/models/concerns/pieces_jointes_list_concern_spec.rb index 0356a52c9..e90e2d4ae 100644 --- a/spec/models/concerns/pieces_jointes_list_concern_spec.rb +++ b/spec/models/concerns/pieces_jointes_list_concern_spec.rb @@ -1,50 +1,78 @@ describe PiecesJointesListConcern do describe '#pieces_jointes_list' do include Logic - let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) } - let(:types_de_champ_public) do - [ - { type: :integer_number, stable_id: 900 }, - { type: :piece_justificative, libelle: "pj1", stable_id: 910 }, - { type: :piece_justificative, libelle: "pj-cond", stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) }, - { type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "pj2", stable_id: 921 }] }, - { type: :titre_identite, libelle: "pj3", stable_id: 930 } - ] - end - let(:types_de_champ_private) do - [ - { type: :integer_number, stable_id: 950 }, - { type: :piece_justificative, libelle: "pj5", stable_id: 960 }, - { type: :piece_justificative, libelle: "pj-cond2", stable_id: 961, condition: ds_eq(champ_value(900), constant(1)) }, - { type: :repetition, libelle: "Répétition2", stable_id: 970, children: [{ type: :piece_justificative, libelle: "pj6", stable_id: 971 }] } - ] - end + describe 'public_wrapped_partionned_pjs and exportables_pieces_jointes' do + let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) } + let(:types_de_champ_public) do + [ + { type: :integer_number, stable_id: 900 }, + { type: :piece_justificative, libelle: "pj1", stable_id: 910 }, + { type: :piece_justificative, libelle: "pj-cond", stable_id: 911, condition: ds_eq(champ_value(900), constant(1)) }, + { type: :repetition, libelle: "Répétition", stable_id: 920, children: [{ type: :piece_justificative, libelle: "pj2", stable_id: 921 }] }, + { type: :titre_identite, libelle: "pj3", stable_id: 930 } + ] + end - let(:types_de_champ) { procedure.active_revision.types_de_champ } - def find_by_stable_id(stable_id) = types_de_champ.find { _1.stable_id == stable_id } + let(:types_de_champ_private) do + [ + { type: :integer_number, stable_id: 950 }, + { type: :piece_justificative, libelle: "pj5", stable_id: 960 }, + { type: :piece_justificative, libelle: "pj-cond2", stable_id: 961, condition: ds_eq(champ_value(900), constant(1)) }, + { type: :repetition, libelle: "Répétition2", stable_id: 970, children: [{ type: :piece_justificative, libelle: "pj6", stable_id: 971 }] } + ] + end - let(:pj1) { find_by_stable_id(910) } - let(:pjcond) { find_by_stable_id(911) } - let(:repetition) { find_by_stable_id(920) } - let(:pj2) { find_by_stable_id(921) } - let(:pj3) { find_by_stable_id(930) } + let(:types_de_champ) { procedure.active_revision.types_de_champ } + def find_by_stable_id(stable_id) = types_de_champ.find { _1.stable_id == stable_id } - let(:pj5) { find_by_stable_id(960) } - let(:pjcond2) { find_by_stable_id(961) } - let(:repetition2) { find_by_stable_id(970) } - let(:pj6) { find_by_stable_id(971) } + let(:pj1) { find_by_stable_id(910) } + let(:pjcond) { find_by_stable_id(911) } + let(:repetition) { find_by_stable_id(920) } + let(:pj2) { find_by_stable_id(921) } + let(:pj3) { find_by_stable_id(930) } - it "returns the list of pieces jointes without conditional" do - expect(procedure.public_wrapped_partionned_pjs.first).to match_array([[pj1], [pj2, repetition], [pj3]]) - end + let(:pj5) { find_by_stable_id(960) } + let(:pjcond2) { find_by_stable_id(961) } + let(:repetition2) { find_by_stable_id(970) } + let(:pj6) { find_by_stable_id(971) } - it "returns the list of pieces jointes having conditional" do - expect(procedure.public_wrapped_partionned_pjs.second).to match_array([[pjcond]]) - end + it "returns the list of pieces jointes without conditional" do + expect(procedure.public_wrapped_partionned_pjs.first).to match_array([[pj1], [pj2, repetition], [pj3]]) + end - it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do - expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle)) + it "returns the list of pieces jointes having conditional" do + expect(procedure.public_wrapped_partionned_pjs.second).to match_array([[pjcond]]) + end + + it "returns the list of pieces jointes with private, without parent repetition, without titre identite" do + expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle)) + end + + it "returns the same list but for all versions" do + expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array([pj1, pj2, pjcond, pj5, pjcond2, pj6].map(&:libelle)) + end end end + + describe '#outdated_exportables_pieces_jointes' do + let(:types_de_champ_public) do + [ + { type: :piece_justificative, libelle: "outdated", stable_id: 1 }, + { type: :piece_justificative, libelle: "kept", stable_id: 2 } + ] + end + + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + + before do + procedure.draft_revision.remove_type_de_champ(1) + procedure.draft_revision.add_type_de_champ(type_champ: :piece_justificative, libelle: 'new', mandatory: false) + procedure.publish_revision! + end + + it { expect(procedure.exportables_pieces_jointes_for_all_versions.map(&:libelle)).to eq(["new", "kept", "outdated"]) } + it { expect(procedure.exportables_pieces_jointes.map(&:libelle)).to match_array(["kept", "new"]) } + it { expect(procedure.outdated_exportables_pieces_jointes.map(&:libelle)).to match_array(["outdated"]) } + end end From 977cfc87ce24f0eca4620f882f0998def5f3dbe4 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Thu, 18 Jul 2024 09:57:23 +0200 Subject: [PATCH 04/16] Add export_item type --- app/models/export_item.rb | 70 +++++++++++++++++++++++++++++ app/types/export_item_type.rb | 40 +++++++++++++++++ config/initializers/types.rb | 5 +++ spec/models/export_item_spec.rb | 20 +++++++++ spec/types/export_item_type_spec.rb | 55 +++++++++++++++++++++++ 5 files changed, 190 insertions(+) create mode 100644 app/models/export_item.rb create mode 100644 app/types/export_item_type.rb create mode 100644 config/initializers/types.rb create mode 100644 spec/models/export_item_spec.rb create mode 100644 spec/types/export_item_type_spec.rb diff --git a/app/models/export_item.rb b/app/models/export_item.rb new file mode 100644 index 000000000..8ee6e4323 --- /dev/null +++ b/app/models/export_item.rb @@ -0,0 +1,70 @@ +class ExportItem + include TagsSubstitutionConcern + DOSSIER_STATE = Dossier.states.fetch(:en_construction) + FORMAT_DATE = "%Y-%m-%d".freeze + + attr_reader :template, :enabled, :stable_id + + def initialize(template:, enabled: true, stable_id: nil) + @template, @enabled, @stable_id = template, enabled, stable_id + end + + def self.default(prefix:, enabled: true, stable_id: nil) + new(template: prefix_dossier_id(prefix), enabled:, stable_id:) + end + + def self.default_pj(tdc) + default(prefix: tdc.libelle_as_filename, enabled: false, stable_id: tdc.stable_id) + end + + def enabled? = enabled + + def template_json = template.to_json + + def template_string = TiptapService.new.to_path(template) + + def path(dossier, attachment: nil, row_index: nil, index: nil) + used_tags = TiptapService.used_tags_and_libelle_for(template) + substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true) + substitutions['original-filename'] = attachment.filename.base if attachment + + TiptapService.new.to_path(template, substitutions) + suffix(attachment, row_index, index) + end + + def ==(other) + self.class == other.class && + template == other.template && + enabled == other.enabled && + stable_id == other.stable_id + end + + private + + def self.prefix_dossier_id(prefix) + { + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { text: "#{prefix}-", type: "text" }, + { type: "mention", attrs: DOSSIER_ID_TAG.slice(:id, :label) } + ] + } + ] + } + end + + def suffix(attachment, row_index, index) + suffix = "" + suffix += "-#{add_one_and_pad(row_index)}" if row_index.present? + suffix += "-#{add_one_and_pad(index)}" if index.present? + suffix += attachment.filename.extension_with_delimiter if attachment + + suffix + end + + def add_one_and_pad(number) + (number + 1).to_s.rjust(2, '0') if number.present? + end +end diff --git a/app/types/export_item_type.rb b/app/types/export_item_type.rb new file mode 100644 index 000000000..182941eef --- /dev/null +++ b/app/types/export_item_type.rb @@ -0,0 +1,40 @@ +class ExportItemType < ActiveRecord::Type::Value + # form_input, or setter -> type + def cast(value) + value = value.deep_symbolize_keys if value.respond_to?(:deep_symbolize_keys) + + case value + in ExportItem + value + in NilClass # default value + nil + # from db + in { template: Hash, enabled: TrueClass | FalseClass } => h + + ExportItem.new(**h.slice(:template, :enabled, :stable_id)) + # from form + in { template: String } => h + + template = JSON.parse(h[:template]).deep_symbolize_keys + enabled = h[:enabled] == 'true' + stable_id = h[:stable_id]&.to_i + ExportItem.new(template:, enabled:, stable_id:) + end + end + + # db -> ruby + def deserialize(value) = cast(value&.then { JSON.parse(_1) }) + + # ruby -> db + def serialize(value) + if value.is_a?(ExportItem) + JSON.generate({ + template: value.template, + enabled: value.enabled, + stable_id: value.stable_id + }.compact) + else + raise ArgumentError, "Invalid value for ExportItem serialization: #{value}" + end + end +end diff --git a/config/initializers/types.rb b/config/initializers/types.rb new file mode 100644 index 000000000..edf12e317 --- /dev/null +++ b/config/initializers/types.rb @@ -0,0 +1,5 @@ +require Rails.root.join("app/types/export_item_type") + +ActiveSupport.on_load(:active_record) do + ActiveRecord::Type.register(:export_item, ExportItemType) +end diff --git a/spec/models/export_item_spec.rb b/spec/models/export_item_spec.rb new file mode 100644 index 000000000..05c3cd41a --- /dev/null +++ b/spec/models/export_item_spec.rb @@ -0,0 +1,20 @@ +describe ExportItem do + describe 'path' do + let(:export_item) { ExportItem.default(prefix: 'custom') } + let(:dossier) { create(:dossier) } + let(:attachment) do + ActiveStorage::Attachment.new( + name: 'filename', + blob: ActiveStorage::Blob.new(filename: "file.pdf") + ) + end + + context 'without index nor row_index' do + it do + expect(export_item.path(dossier, attachment:)).to eq("custom-#{dossier.id}.pdf") + expect(export_item.path(dossier, attachment:, index: 3)).to eq("custom-#{dossier.id}-04.pdf") + expect(export_item.path(dossier, attachment:, row_index: 2, index: 3)).to eq("custom-#{dossier.id}-03-04.pdf") + end + end + end +end diff --git a/spec/types/export_item_type_spec.rb b/spec/types/export_item_type_spec.rb new file mode 100644 index 000000000..1a0d270b0 --- /dev/null +++ b/spec/types/export_item_type_spec.rb @@ -0,0 +1,55 @@ +describe ExportItemType do + let(:type) { ExportItemType.new } + + describe 'cast' do + it 'from ExportItem' do + export_item = ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42) + expect(type.cast(export_item)).to eq(export_item) + end + + it 'from nil' do + expect(type.cast(nil)).to eq(nil) + end + + it 'from db' do + h = { template: { foo: 'bar' }, enabled: true, stable_id: 42 } + expect(type.cast(h)).to eq(ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42)) + end + + it 'from form' do + h = { template: '{"foo":{"bar":"zob"}}' } + expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: false)) + + h = { template: '{"foo":{"bar":"zob"}}', enabled: 'true' } + expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: true)) + + h = { template: '{"foo":{"bar":"zob"}}', stable_id: '42' } + expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: false, stable_id: 42)) + + h = { template: '{"foo":{"bar":"zob"}}', enabled: 'true', stable_id: '42' } + expect(type.cast(h)).to eq(ExportItem.new(template: { foo: { bar: 'zob' } }, enabled: true, stable_id: 42)) + end + + it 'from invalid value' do + expect { type.cast('invalid value') }.to raise_error(NoMatchingPatternError) + end + end + + describe 'deserialize' do + it 'from nil' do + expect(type.deserialize(nil)).to eq(nil) + end + + it 'from db' do + h = { template: { foo: 'bar' }, enabled: true, stable_id: 42 } + expect(type.deserialize(JSON.generate(h))).to eq(ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42)) + end + end + + describe 'serialize' do + it 'from ExportItem' do + export_item = ExportItem.new(template: { foo: 'bar' }, enabled: true, stable_id: 42) + expect(type.serialize(export_item)).to eq('{"template":{"foo":"bar"},"enabled":true,"stable_id":42}') + end + end +end From 8906a596357a98bb9ad1ba87615d1efcb8e0b4c3 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 23 Jul 2024 17:05:16 +0200 Subject: [PATCH 05/16] simplify export_template model and spec --- app/models/export_template.rb | 173 +++------ spec/factories/export_template.rb | 83 +---- spec/models/export_spec.rb | 5 +- spec/models/export_template_spec.rb | 340 +++--------------- .../services/procedure_export_service_spec.rb | 7 +- .../procedure_export_service_zip_spec.rb | 12 +- 6 files changed, 99 insertions(+), 521 deletions(-) diff --git a/app/models/export_template.rb b/app/models/export_template.rb index f3ee41235..0f5210a42 100644 --- a/app/models/export_template.rb +++ b/app/models/export_template.rb @@ -1,157 +1,66 @@ class ExportTemplate < ApplicationRecord include TagsSubstitutionConcern + self.ignored_columns += ["content"] + belongs_to :groupe_instructeur has_one :procedure, through: :groupe_instructeur has_many :exports, dependent: :nullify + + enum kind: { zip: "zip" }, _prefix: :template + + attribute :dossier_folder, :export_item + attribute :export_pdf, :export_item + attribute :pjs, :export_item, array: true + + before_validation :ensure_pjs_are_legit + validates_with ExportTemplateValidator DOSSIER_STATE = Dossier.states.fetch(:en_construction) - FORMAT_DATE = "%Y-%m-%d" - def set_default_values - content["default_dossier_directory"] = tiptap_json("dossier-") - content["pdf_name"] = tiptap_json("export_") - - content["pjs"] = [] - procedure.exportables_pieces_jointes.each do |pj| - content["pjs"] << { "stable_id" => pj.stable_id.to_s, "path" => tiptap_json("#{pj.libelle.parameterize}-") } - end + # when a pj has been added to a revision, it will not be present in the previous pjs + # a default value is provided. + def pj(tdc) + pjs.find { _1.stable_id == tdc.stable_id } || ExportItem.default_pj(tdc) end - def tiptap_default_dossier_directory=(body) - self.content["default_dossier_directory"] = JSON.parse(body) + def self.default(name: nil, kind: 'zip', groupe_instructeur:) + dossier_folder = ExportItem.default(prefix: 'dossier') + export_pdf = ExportItem.default(prefix: 'export') + pjs = groupe_instructeur.procedure.exportables_pieces_jointes.map { |tdc| ExportItem.default_pj(tdc) } + + new(name:, kind:, groupe_instructeur:, dossier_folder:, export_pdf:, pjs:) end - def tiptap_default_dossier_directory - tiptap_content("default_dossier_directory") - end - - def tiptap_pdf_name=(body) - self.content["pdf_name"] = JSON.parse(body) - end - - def tiptap_pdf_name - tiptap_content("pdf_name") - end - - def content_for_pj(pj) - content_for_pj_id(pj.stable_id)&.to_json - end - - def assign_pj_names(pj_params) - self.content["pjs"] = [] - pj_params.each do |pj_param| - self.content["pjs"] << { stable_id: pj_param[0].delete_prefix("tiptap_pj_"), path: JSON.parse(pj_param[1]) } - end - end - - def attachment_and_path(dossier, attachment, index: 0, row_index: nil, champ: nil) - [ - attachment, - path(dossier, attachment, index:, row_index:, champ:) - ] - end - - def tiptap_convert(dossier, param) - if content[param]["content"]&.first&.[]("content") - render_attributes_for(content[param], dossier) - end - end - - def tiptap_convert_pj(dossier, pj_stable_id, attachment = nil) - if content_for_pj_id(pj_stable_id)["content"]&.first&.[]("content") - render_attributes_for(content_for_pj_id(pj_stable_id), dossier, attachment) - end - end - - def render_attributes_for(content_for, dossier, attachment = nil) - used_tags = TiptapService.used_tags_and_libelle_for(content_for.deep_symbolize_keys) - substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true) - substitutions['original-filename'] = attachment.filename.base if attachment - TiptapService.new.to_path(content_for.deep_symbolize_keys, substitutions) - end - - def specific_tags + def tags tags_categorized.slice(:individual, :etablissement, :dossier).values.flatten end - def tags_for_pj - specific_tags.push({ + def pj_tags + tags.push( libelle: 'nom original du fichier', - id: 'original-filename', - maybe_null: false - }) + id: 'original-filename' + ) + end + + def attachment_path(dossier, attachment, index: 0, row_index: nil, champ: nil) + file_path = if attachment.name == 'pdf_export_for_instructeur' + export_pdf.path(dossier, attachment:) + elsif attachment.record_type == 'Champ' && pj(champ.type_de_champ).enabled? + pj(champ.type_de_champ).path(dossier, attachment:, index:, row_index:) + else + nil + end + + File.join(dossier_folder.path(dossier), file_path) if file_path.present? end private - def tiptap_content(key) - content[key]&.to_json - end + def ensure_pjs_are_legit + legitimate_pj_stable_ids = procedure.exportables_pieces_jointes_for_all_versions.map(&:stable_id) - def tiptap_json(prefix) - { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => prefix, "type" => "text" }, { "type" => "mention", "attrs" => DOSSIER_ID_TAG.stringify_keys }] } - ] - } - end - - def content_for_pj_id(stable_id) - content_for_stable_id = content["pjs"].find { _1.symbolize_keys[:stable_id] == stable_id.to_s } - content_for_stable_id.symbolize_keys.fetch(:path) - end - - def folder(dossier) - render_attributes_for(content["default_dossier_directory"], dossier) - end - - def export_path(dossier) - File.join(folder(dossier), export_filename(dossier)) - end - - def export_filename(dossier) - "#{render_attributes_for(content["pdf_name"], dossier)}.pdf" - end - - def path(dossier, attachment, index: 0, row_index: nil, champ: nil) - if attachment.name == 'pdf_export_for_instructeur' - return export_path(dossier) - end - - dir_path = case attachment.record_type - when 'Dossier' - 'dossier' - when 'Commentaire' - 'messagerie' - when 'Avis' - 'avis' - when 'Attestation', 'Etablissement' - 'pieces_justificatives' - else - # for attachment - return attachment_path(dossier, attachment, index, row_index, champ) - end - - File.join(folder(dossier), dir_path, attachment.filename.to_s) - end - - def attachment_path(dossier, attachment, index, row_index, champ) - stable_id = champ.stable_id - tiptap_pj = content["pjs"].find { |pj| pj["stable_id"] == stable_id.to_s } - if tiptap_pj - File.join(folder(dossier), tiptap_convert_pj(dossier, stable_id, attachment) + suffix(attachment, index, row_index)) - else - File.join(folder(dossier), "erreur_renommage", attachment.filename.to_s) - end - end - - def suffix(attachment, index, row_index) - suffix = "-#{index + 1}" - suffix += "-#{row_index + 1}" if row_index.present? - - suffix + attachment.filename.extension_with_delimiter + self.pjs = pjs.filter { _1.stable_id.in?(legitimate_pj_stable_ids) } end end diff --git a/spec/factories/export_template.rb b/spec/factories/export_template.rb index 6785356af..875b08553 100644 --- a/spec/factories/export_template.rb +++ b/spec/factories/export_template.rb @@ -2,86 +2,11 @@ FactoryBot.define do factory :export_template do name { "Mon export" } groupe_instructeur - content { - { - "pdf_name" => - { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_id", "label" => "id dossier" } }, { "text" => " .pdf", "type" => "text" }] } - ] - }, - "default_dossier_directory" => { - "type" => "doc", - "content" => - [ - { - "type" => "paragraph", - "content" => - [ - { "text" => "dossier_", "type" => "text" }, - { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, - { "text" => " ", "type" => "text" } - ] - } - ] - } - } -} - kind { "zip" } + initialize_with { ExportTemplate.default(name:, groupe_instructeur: groupe_instructeur) } - to_create do |export_template, _context| - export_template.set_default_values - export_template.save - end - - trait :with_custom_content do - to_create do |export_template, context| - export_template.set_default_values - export_template.content = context.content - export_template.save - end - end - - trait :with_custom_ddd_prefix do - transient do - ddd_prefix { 'dossier_' } - end - - to_create do |export_template, context| - export_template.set_default_values - export_template.content["default_dossier_directory"]["content"] = [ - { - "type" => "paragraph", - "content" => - [ - { "text" => context.ddd_prefix, "type" => "text" }, - { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, - { "text" => " ", "type" => "text" } - ] - } - ] - export_template.save - end - end - - trait :with_date_depot_for_export_pdf do - to_create do |export_template, _| - export_template.set_default_values - export_template.content["pdf_name"]["content"] = [ - { - "type" => "paragraph", - "content" => - [ - { "text" => "export_", "type" => "text" }, - { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, - { "text" => "-", "type" => "text" }, - { "type" => "mention", "attrs" => { "id" => "dossier_depose_at", "label" => "date de dépôt" } }, - { "text" => " ", "type" => "text" } - ] - } - ] - export_template.save + trait :enabled_pjs do + after(:build) do |export_template, _evaluator| + export_template.pjs.each { _1.instance_variable_set('@enabled', true) } end end end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 5b2a8ae99..bd950bc36 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -110,9 +110,10 @@ RSpec.describe Export, type: :model do end context 'with export template' do - let(:export_template) { build(:export_template) } + let(:export_template) { create(:export_template, groupe_instructeur: gi_1) } + it 'creates new export' do - expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, export_template: export_template, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) } + expect { Export.find_or_create_fresh_export(:zip, [gi_1], instructeur, export_template:, time_span_type: Export.time_span_types.fetch(:everything), statut: Export.statuts.fetch(:tous), procedure_presentation: pp) } .to change { Export.count }.by(1) end end diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb index 595668aa2..839b9c015 100644 --- a/spec/models/export_template_spec.rb +++ b/spec/models/export_template_spec.rb @@ -1,8 +1,7 @@ describe ExportTemplate do let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) } - let(:export_template) { create(:export_template, :with_custom_content, groupe_instructeur:, content:) } - let(:procedure) { create(:procedure_with_dossiers, types_de_champ_public:, for_individual:) } - let(:dossier) { procedure.dossiers.first } + let(:export_template) { build(:export_template, groupe_instructeur:) } + let(:procedure) { create(:procedure, types_de_champ_public:, for_individual:) } let(:for_individual) { false } let(:types_de_champ_public) do [ @@ -10,335 +9,80 @@ describe ExportTemplate do { type: :titre_identite, libelle: "CNI", mandatory: true, stable_id: 5 } ] end - let(:content) do - { - "pdf_name" => { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } - ] - }, - "default_dossier_directory" => { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] } - ] - }, - "pjs" => - [ - { path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "original-filename", "label" => "nom original du fichier" } }, { "text" => " _justif", "type" => "text" }] }] }, stable_id: "3" }, - { - path: - { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "cni_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] }, - stable_id: "5" - }, - { - path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "pj_repet_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] }, - stable_id: "10" - } - ] - } - end - describe 'new' do - let(:export_template) { build(:export_template, groupe_instructeur: groupe_instructeur) } + describe '.default' do it 'set default values' do - export_template.set_default_values - expect(export_template.content).to eq({ - "pdf_name" => { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] } - ] - }, - "default_dossier_directory" => { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "dossier-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] } - ] - }, - "pjs" => - [ - - { - "stable_id" => "3", - "path" => { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "justificatif-de-domicile-", "type" => "text" }, { "type" => "mention", "attrs" => ExportTemplate::DOSSIER_ID_TAG.stringify_keys }] }] } - } - ] - }) + expect(export_template.export_pdf).to eq(ExportItem.default(prefix: "export", enabled: true)) + expect(export_template.dossier_folder).to eq(ExportItem.default(prefix: "dossier", enabled: true)) + expect(export_template.pjs).to eq([ExportItem.default(stable_id: 3, prefix: "justificatif-de-domicile", enabled: false)]) end end - describe '#assign_pj_names' do - let(:pj_params) do - { - "tiptap_pj_1" => { - "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }] - }.to_json - } + describe '#pj' do + context 'when pj exists' do + subject { export_template.pj(double(stable_id: 3)) } + + it { is_expected.to eq(ExportItem.default(stable_id: 3, prefix: "justificatif-de-domicile", enabled: false)) } end - it 'values content from pj params' do - export_template.assign_pj_names(pj_params) - expect(export_template.content["pjs"]).to eq [ - { :path => { "content" => [{ "content" => [{ "text" => "avis-commission-", "type" => "text" }, { "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" }, "type" => "mention" }], "type" => "paragraph" }], "type" => "doc" }, :stable_id => "1" } - ] + + context 'when pj does not exist' do + subject { export_template.pj(TypeDeChamp.new(libelle: 'hi', stable_id: 10)) } + + it { is_expected.to eq(ExportItem.default(stable_id: 10, prefix: "hi", enabled: false)) } end end - describe '#tiptap_default_dossier_directory' do - it 'returns tiptap_default_dossier_directory from content' do - expect(export_template.tiptap_default_dossier_directory).to eq({ - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] } - ] - }.to_json) - end - end - - describe '#tiptap_pdf_name' do - it 'returns tiptap_pdf_name from content' do - expect(export_template.tiptap_pdf_name).to eq({ - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } - ] - }.to_json) - end - end - - describe '#content_for_pj' do - let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) } - let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) } - - let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } - - it 'returns tiptap content for pj' do - expect(export_template.content_for_pj(type_de_champ_pj)).to eq({ - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "original-filename", "label" => "nom original du fichier" } }, { "text" => " _justif", "type" => "text" }] } - ] - }.to_json) - end - end - - describe '#attachment_and_path' do - let(:dossier) { create(:dossier) } + describe '#attachment_path' do + let(:dossier) { create(:dossier, :en_construction, procedure:) } context 'for export pdf' do - let(:attachment) { double("attachment") } + let(:export_template) do + build(:export_template, groupe_instructeur:, dossier_folder: ExportItem.default(prefix: "DOSSIER"), export_pdf: ExportItem.default(prefix: "mon_export")) + end + + let(:attachment) { ActiveStorage::Attachment.new(name: 'pdf_export_for_instructeur', blob: ActiveStorage::Blob.new(filename: "export.pdf")) } it 'gives absolute filename for export of specific dossier' do - allow(attachment).to receive(:name).and_return('pdf_export_for_instructeur') - expect(export_template.attachment_and_path(dossier, attachment)).to eq([attachment, "DOSSIER_#{dossier.id}/mon_export_#{dossier.id}.pdf"]) + expect(export_template.attachment_path(dossier, attachment)).to eq("DOSSIER-#{dossier.id}/mon_export-#{dossier.id}.pdf") end end context 'for pj' do - let(:dossier) { create(:dossier, :with_populated_champs, procedure:) } - let(:champ_pj) { dossier.champs.find(&:piece_justificative?) } + let(:champ_pj) { dossier.champs_public.first } + let(:export_template) { create(:export_template, groupe_instructeur:, pjs: [ExportItem.default(stable_id: 3, prefix: "justif", enabled: true)]) } + let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } it 'returns pj and custom name for pj' do - expect(export_template.attachment_and_path(dossier, attachment, champ: champ_pj)).to eq([attachment, "DOSSIER_#{dossier.id}/superpj_justif-1.png"]) - end - end - context 'pj repetable' do - let(:procedure) { create(:procedure, :for_individual, types_de_champ_public:) } - let(:dossier) { create(:dossier, :with_populated_champs, procedure:) } - let(:draft) { procedure.draft_revision } - let(:types_de_champ_public) do - [ - { - type: :repetition, - stable_id: 3333, - mandatory: true, children: [ - { type: :text, libelle: 'sub type de champ' }, - { type: :piece_justificative, stable_id: 10, libelle: 'pj repet' } - ] - } - ] - end - let(:champ_pj) { dossier.champs.find(&:piece_justificative?) } - let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } - - it 'rename repetable pj' do - expect(export_template.attachment_and_path(dossier, attachment, champ: champ_pj)).to eq([attachment, "DOSSIER_#{dossier.id}/pj_repet_#{dossier.id}-1.png"]) + expect(export_template.attachment_path(dossier, attachment, champ: champ_pj)).to eq("dossier-#{dossier.id}/justif-#{dossier.id}-01.png") end end end - describe '#tiptap_convert' do - it 'convert default dossier directory' do - expect(export_template.tiptap_convert(procedure.dossiers.first, "default_dossier_directory")).to eq "DOSSIER_#{dossier.id}" - end + describe '#tags and #pj_tags' do + let(:procedure) { build(:procedure, for_individual:) } - it 'convert pdf_name' do - expect(export_template.tiptap_convert(procedure.dossiers.first, "pdf_name")).to eq "mon_export_#{dossier.id}" - end - - context 'for date' do - let(:export_template) { create(:export_template, :with_date_depot_for_export_pdf, groupe_instructeur:) } - let(:dossier) { create(:dossier, :en_construction, procedure:, depose_at: Date.parse("2024/03/30")) } - it 'convert date with dash' do - expect(export_template.tiptap_convert(dossier, "pdf_name")).to eq "export_#{dossier.id}-2024-03-30" - end - end - end - - describe '#tiptap_convert_pj' do - let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile' }]) } - let(:dossier) { create(:dossier, :with_populated_champs, procedure:) } - let(:champ_pj) { dossier.champs.first } - let(:attachment) { ActiveStorage::Attachment.new(name: 'pj', record: champ_pj, blob: ActiveStorage::Blob.new(filename: "superpj.png")) } - - it 'convert pj' do - attachment - expect(export_template.tiptap_convert_pj(dossier, 3, attachment)).to eq "superpj_justif" - end - end - - describe '#valid?' do - let(:subject) { build(:export_template, groupe_instructeur:, content:) } - let(:ddd_text) { "DoSSIER" } - let(:mention) { { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } } } - let(:ddd_mention) { mention } - let(:pdf_text) { "export" } - let(:pdf_mention) { mention } - let(:pj_text) { "_pj" } - let(:pj_mention) { mention } - let(:content) do - { - "pdf_name" => { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => pdf_text, "type" => "text" }, pdf_mention] } - ] - }, - "default_dossier_directory" => { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => ddd_text, "type" => "text" }, ddd_mention] } - ] - }, - "pjs" => - [ - { path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [pj_mention, { "text" => pj_text, "type" => "text" }] }] }, stable_id: "3" } - ] - } - end - - context 'with valid default dossier directory' do - it 'has no error for default_dossier_directory' do - expect(subject.valid?).to be_truthy - end - end - - context 'with no ddd text' do - let(:ddd_text) { " " } - context 'with mention' do - let(:ddd_mention) { { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } } } - it 'has no error for default_dossier_directory' do - expect(subject.valid?).to be_truthy - end + context 'for entreprise procedure' do + let(:for_individual) { false } + let(:expected_tags) do + ['entreprise_siren', 'entreprise_numero_tva_intracommunautaire', 'entreprise_siret_siege_social', 'entreprise_raison_sociale', 'entreprise_adresse', 'dossier_depose_at', 'dossier_procedure_libelle', 'dossier_service_name', 'dossier_number', 'dossier_groupe_instructeur'] end - context 'without numéro de dossier' do - let(:ddd_mention) { { "type" => "mention", "attrs" => { "id" => 'dossier_service_name', "label" => "nom du service" } } } - it "add error for tiptap_default_dossier_directory" do - expect(subject.valid?).to be_falsey - expect(subject.errors[:tiptap_default_dossier_directory]).to be_present - expect(subject.errors.full_messages).to include "Le champ « Nom du répertoire » doit contenir le numéro du dossier" - end - end - end - - context 'with valid pdf name' do - it 'has no error for pdf name' do - expect(subject.valid?).to be_truthy - expect(subject.errors[:tiptap_pdf_name]).not_to be_present - end - end - - context 'with pdf text and without mention' do - let(:pdf_text) { "export" } - let(:pdf_mention) { { "type" => "mention", "attrs" => {} } } - - it "add no error" do - expect(subject.valid?).to be_truthy - end - end - - context 'with no pdf text' do - let(:pdf_text) { " " } - - context 'with mention' do - it 'has no error for default_dossier_directory' do - expect(subject.valid?).to be_truthy - expect(subject.errors[:tiptap_pdf_name]).not_to be_present - end - end - - context 'without mention' do - let(:pdf_mention) { { "type" => "mention", "attrs" => {} } } - it "add error for pdf name" do - expect(subject.valid?).to be_falsey - expect(subject.errors.full_messages).to include "Le champ « Nom du dossier au format pdf » doit être rempli" - end - end - end - - context 'with no pj text' do - # let!(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) } - let(:pj_text) { " " } - - context 'with mention' do - it 'has no error for pj' do - expect(subject.valid?).to be_truthy - end - end - - context 'without mention' do - let(:pj_mention) { { "type" => "mention", "attrs" => {} } } - it "add error for pj" do - expect(subject.valid?).to be_falsey - expect(subject.errors.full_messages).to include "Le champ « Justificatif de domicile » doit être rempli" - end - end - end - end - - context 'for entreprise procedure' do - let(:for_individual) { false } - describe 'specific_tags' do it do - tags = export_template.specific_tags - expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"] + expect(export_template.tags.map { _1[:id] }).to eq(expected_tags) + expect(export_template.pj_tags.map { _1[:id] }).to eq(expected_tags + ['original-filename']) end end - describe 'tags_for_pj' do - it do - tags = export_template.tags_for_pj - expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur", "original-filename"] + context 'for individual procedure' do + let(:for_individual) { true } + let(:expected_tags) do + ['individual_gender', 'individual_last_name', 'individual_first_name', 'dossier_depose_at', 'dossier_procedure_libelle', 'dossier_service_name', 'dossier_number', 'dossier_groupe_instructeur'] end - end - end - context 'for individual procedure' do - let(:for_individual) { true } - describe 'specific_tags' do it do - tags = export_template.specific_tags - expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"] - end - end - - describe 'tags_for_pj' do - it do - tags = export_template.tags_for_pj - expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur", "original-filename"] + expect(export_template.tags.map { _1[:id] }).to eq(expected_tags) + expect(export_template.pj_tags.map { _1[:id] }).to eq(expected_tags + ['original-filename']) end end end diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index f00471505..5863ea454 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -450,7 +450,7 @@ describe ProcedureExportService do context 'with export_template' do let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) } - let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) } + let(:export_template) { create(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) } before do allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") end @@ -465,10 +465,9 @@ describe ProcedureExportService do structure = [ "#{base_fn}/", "#{base_fn}/dossier-#{dossier.id}/", - "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-1.txt", - "#{base_fn}/dossier-#{dossier.id}/export_#{dossier.id}.pdf" + "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-01.txt", + "#{base_fn}/dossier-#{dossier.id}/export-#{dossier.id}.pdf" ] - expect(files.size).to eq(structure.size) expect(files.map(&:filename)).to match_array(structure) end FileUtils.remove_entry_secure('tmp.zip') diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb index e4daaf3ee..4b8b78e60 100644 --- a/spec/services/procedure_export_service_zip_spec.rb +++ b/spec/services/procedure_export_service_zip_spec.rb @@ -2,7 +2,7 @@ describe ProcedureExportService do let(:instructeur) { create(:instructeur) } let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative, libelle: 'repet_pj' }] }]) } let(:dossiers) { create_list(:dossier, 10, procedure: procedure) } - let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) } + let(:export_template) { create(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) } let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp') @@ -49,11 +49,11 @@ describe ProcedureExportService do structure = [ "export/", "export/dossier-#{dossier.id}/", - "export/dossier-#{dossier.id}/export_#{dossier.id}.pdf", - "export/dossier-#{dossier.id}/pj-#{dossier.id}-1.png", - "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-1-1.png", - "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-2-1.png", - "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-1-2.png" + "export/dossier-#{dossier.id}/export-#{dossier.id}.pdf", + "export/dossier-#{dossier.id}/pj-#{dossier.id}-01.png", + "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-01-01.png", + "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-02-01.png", + "export/dossier-#{dossier.id}/repet_pj-#{dossier.id}-01-02.png" ] expect(files.size).to eq(dossiers.count * 6 + 1) From 33244f6d700bcc9c4eb790eaedcad8b26efc6ff6 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 23 Jul 2024 17:08:39 +0200 Subject: [PATCH 06/16] adapt pieces_justificatives_service to new export_template.attachment_path --- app/services/pieces_justificatives_service.rb | 18 +-- .../pieces_justificatives_service_spec.rb | 107 +++++++++--------- 2 files changed, 64 insertions(+), 61 deletions(-) diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 92d53b088..e3972d67a 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -26,7 +26,7 @@ class PiecesJustificativesService docs += signatures(bill_ids.uniq) end - docs + docs.filter { |_attachment, path| path.present? } end def generate_dossiers_export(dossiers) # TODO: renommer generate_dossier_export sans s @@ -46,14 +46,14 @@ class PiecesJustificativesService }) a = ActiveStorage::FakeAttachment.new( file: StringIO.new(pdf), - filename: "export-#{dossier.id}.pdf", + filename: ActiveStorage::Filename.new("export-#{dossier.id}.pdf"), name: 'pdf_export_for_instructeur', id: dossier.id, created_at: dossier.updated_at ) if @export_template - pdfs << @export_template.attachment_and_path(dossier, a) + pdfs << [a, @export_template.attachment_path(dossier, a)] else pdfs << ActiveStorage::DownloadableFile.pj_and_path(dossier.id, a) end @@ -148,7 +148,7 @@ class PiecesJustificativesService row_index = champs_id_row_index[champ.id] if @export_template - @export_template.attachment_and_path(champ.dossier, attachment, index:, row_index:, champ:) + [attachment, @export_template.attachment_path(champ.dossier, attachment, index:, row_index:, champ:)] else ActiveStorage::DownloadableFile.pj_and_path(champ.dossier_id, attachment) end @@ -171,7 +171,7 @@ class PiecesJustificativesService dossier_id = commentaire_id_dossier_id[a.record_id] if @export_template dossier = dossiers.find { _1.id == dossier_id } - @export_template.attachment_and_path(dossier, a) + [a, @export_template.attachment_path(dossier, a)] else ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) end @@ -197,7 +197,7 @@ class PiecesJustificativesService dossier_id = etablissement_id_dossier_id[a.record_id] if @export_template dossier = dossiers.find { _1.id == dossier_id } - @export_template.attachment_and_path(dossier, a) + [a, @export_template.attachment_path(dossier, a)] else ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) end @@ -213,7 +213,7 @@ class PiecesJustificativesService dossier_id = a.record_id if @export_template dossier = dossiers.find { _1.id == dossier_id } - @export_template.attachment_and_path(dossier, a) + [a, @export_template.attachment_path(dossier, a)] else ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) end @@ -234,7 +234,7 @@ class PiecesJustificativesService dossier_id = attestation_id_dossier_id[a.record_id] if @export_template dossier = dossiers.find { _1.id == dossier_id } - @export_template.attachment_and_path(dossier, a) + [a, @export_template.attachment_path(dossier, a)] else ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) end @@ -263,7 +263,7 @@ class PiecesJustificativesService dossier_id = avis_ids_dossier_id[a.record_id] if @export_template dossier = dossiers.find { _1.id == dossier_id } - @export_template.attachment_and_path(dossier, a) + [a, @export_template.attachment_path(dossier, a)] else ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a) end diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 5d731e76a..900c12a05 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -25,7 +25,7 @@ describe PiecesJustificativesService do before { attach_file_to_champ(champ) } it do - expect(export_template).to receive(:attachment_and_path) + expect(export_template).to receive(:attachment_path) .with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:) subject end @@ -40,10 +40,10 @@ describe PiecesJustificativesService do end it do - expect(export_template).to receive(:attachment_and_path) + expect(export_template).to receive(:attachment_path) .with(dossier, attachments(pj_champ(dossier)).first, index: 0, row_index: nil, champ:) - expect(export_template).to receive(:attachment_and_path) + expect(export_template).to receive(:attachment_path) .with(dossier, attachments(pj_champ(dossier)).second, index: 1, row_index: nil, champ:) subject end @@ -66,13 +66,13 @@ describe PiecesJustificativesService do first_child_attachments = attachments(repetition(dossier).champs.first) second_child_attachments = attachments(repetition(dossier).champs.second) - expect(export_template).to receive(:attachment_and_path) + expect(export_template).to receive(:attachment_path) .with(dossier, first_child_attachments.first, index: 0, row_index: 0, champ: first_champ) - expect(export_template).to receive(:attachment_and_path) + expect(export_template).to receive(:attachment_path) .with(dossier, first_child_attachments.second, index: 1, row_index: 0, champ: first_champ) - expect(export_template).to receive(:attachment_and_path) + expect(export_template).to receive(:attachment_path) .with(dossier, second_child_attachments.first, index: 0, row_index: 1, champ: second_champ) count = 0 @@ -90,6 +90,7 @@ describe PiecesJustificativesService do describe '.liste_documents' do let(:dossier) { create(:dossier, procedure: procedure) } let(:dossiers) { Dossier.where(id: dossier.id) } + let(:default_export_template) { build(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) } let(:export_template) { nil } subject do PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:first) @@ -99,39 +100,39 @@ describe PiecesJustificativesService do let(:user_profile) { build(:administrateur) } let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative }]) } let(:witness) { create(:dossier, procedure: procedure) } - let(:pj_champ) { -> (d) { d.champs_public.find { |c| c.type == 'Champs::PieceJustificativeChamp' } } } + def pj_champ(d) = d.champs_public.find { |c| c.type == 'Champs::PieceJustificativeChamp' } context 'with a single attachment' do before do - attach_file_to_champ(pj_champ.call(dossier)) - attach_file_to_champ(pj_champ.call(witness)) + attach_file_to_champ(pj_champ(dossier)) + attach_file_to_champ(pj_champ(witness)) end - it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + it { expect(subject).to match_array(pj_champ(dossier).piece_justificative_file.attachments) } context 'with export_template' do - let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) } - it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + let(:export_template) { build(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) } + + it { expect(subject).to match_array(pj_champ(dossier).piece_justificative_file.attachments) } end end context 'with a multiple attachments' do before do - attach_file_to_champ(pj_champ.call(dossier)) - attach_file_to_champ(pj_champ.call(witness)) - attach_file_to_champ(pj_champ.call(dossier)) + attach_file_to_champ(pj_champ(dossier)) + attach_file_to_champ(pj_champ(witness)) + attach_file_to_champ(pj_champ(dossier)) end it { expect(subject.count).to eq(2) } - it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) } + it { expect(subject).to match_array(pj_champ(dossier).piece_justificative_file.attachments) } end context 'with a pj not safe on a champ' do let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative }]) } let(:dossier) { create(:dossier, procedure: procedure) } - let(:pj_champ) { -> (d) { d.champs_public.find { |c| c.type == 'Champs::PieceJustificativeChamp' } } } - before { attach_file_to_champ(pj_champ.call(dossier), safe = false) } + before { attach_file_to_champ(pj_champ(dossier), false) } it { expect(subject).to be_empty } end @@ -139,7 +140,6 @@ describe PiecesJustificativesService do context 'with a identite champ pj' do let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :titre_identite }]) } let(:dossier) { create(:dossier, procedure: procedure) } - let(:witness) { create(:dossier, procedure: procedure) } let(:champ_identite) { dossier.champs_public.find { |c| c.type == 'Champs::TitreIdentiteChamp' } } @@ -149,6 +149,12 @@ describe PiecesJustificativesService do expect(champ_identite.piece_justificative_file).to be_attached expect(subject).to be_empty end + + context 'with export_template' do + let(:export_template) { build(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) } + + it { expect(subject).to be_empty } + end end context 'with a pj on an commentaire' do @@ -166,10 +172,9 @@ describe PiecesJustificativesService do it { expect(subject).to match_array(dossier.commentaires.first.piece_jointe.attachments) } context 'with export_template' do - let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) } - it 'uses specific name for dossier directory' do - expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/messagerie")).to be true - end + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } end end @@ -177,7 +182,7 @@ describe PiecesJustificativesService do let(:dossier) { create(:dossier) } let!(:commentaire) { create(:commentaire, dossier: dossier) } - before { attach_file(commentaire.piece_jointe, safe = false) } + before { attach_file(commentaire.piece_jointe, false) } it { expect(subject).to be_empty } end @@ -189,17 +194,16 @@ describe PiecesJustificativesService do it { expect(subject).to match_array(dossier.justificatif_motivation.attachment) } context 'with export_template' do - let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) } - it 'uses specific name for dossier directory' do - expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/dossier")).to be true - end + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } end end context 'with a motivation not safe' do let(:dossier) { create(:dossier) } - before { attach_file(dossier.justificatif_motivation, safe = false) } + before { attach_file(dossier.justificatif_motivation, false) } it { expect(subject).to be_empty } end @@ -214,10 +218,9 @@ describe PiecesJustificativesService do end context 'with export_template' do - let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) } - it 'uses specific name for dossier directory' do - expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/pieces_justificatives")).to be true - end + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } end end @@ -242,10 +245,9 @@ describe PiecesJustificativesService do end context 'with export_template' do - let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) } - it 'uses specific name for dossier directory' do - expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/pieces_justificatives")).to be true - end + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } end end end @@ -256,32 +258,33 @@ describe PiecesJustificativesService do let(:witness) { create(:dossier, procedure: procedure) } let!(:private_pj) { create(:type_de_champ_piece_justificative, procedure: procedure, private: true) } - let(:private_pj_champ) { -> (d) { d.champs_private.find { |c| c.type == 'Champs::PieceJustificativeChamp' } } } + def private_pj_champ(d) = d.champs_private.find { |c| c.type == 'Champs::PieceJustificativeChamp' } before do - attach_file_to_champ(private_pj_champ.call(dossier)) - attach_file_to_champ(private_pj_champ.call(witness)) + attach_file_to_champ(private_pj_champ(dossier)) + attach_file_to_champ(private_pj_champ(witness)) end context 'given an administrateur' do let(:user_profile) { build(:administrateur) } - it { expect(subject).to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) } + it { expect(subject).to match_array(private_pj_champ(dossier).piece_justificative_file.attachments) } end context 'given an instructeur' do let(:user_profile) { create(:instructeur) } - it { expect(subject).to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) } + it { expect(subject).to match_array(private_pj_champ(dossier).piece_justificative_file.attachments) } end context 'given an expert' do let(:user_profile) { create(:expert) } - it { expect(subject).not_to match_array(private_pj_champ.call(dossier).piece_justificative_file.attachments) } + it { expect(subject).not_to match_array(private_pj_champ(dossier).piece_justificative_file.attachments) } end end context 'acl on bill' do let(:dossier) { create(:dossier) } let(:witness) { create(:dossier) } + let(:default_export_template) { build(:export_template, groupe_instructeur: dossier.procedure.defaut_groupe_instructeur) } let(:bill_signature) do bs = build(:bill_signature, :with_serialized, :with_signature) @@ -361,6 +364,12 @@ describe PiecesJustificativesService do it "return confidentiel avis.piece_justificative_file" do expect(subject.size).to eq(2) end + + context 'with export_template' do + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } + end end context 'given an instructeur' do @@ -415,13 +424,6 @@ describe PiecesJustificativesService do it "return confidentiel avis.piece_justificative_file" do expect(subject.size).to eq(2) end - - context 'with export_template' do - let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) } - it 'uses specific name for dossier directory' do - expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/avis")).to be true - end - end end context 'given an expert' do @@ -464,11 +466,12 @@ describe PiecesJustificativesService do end context 'with export template' do - let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) } + let(:groupe_instructeur) { procedure.defaut_groupe_instructeur } + let(:export_template) { create(:export_template, groupe_instructeur:, dossier_folder: ExportItem.default(prefix: 'DOSSIER')) } subject { PiecesJustificativesService.new(user_profile:, export_template:).generate_dossiers_export(dossiers) } it 'gives custom name to export pdf file' do - expect(subject.first.second).to eq "DOSSIER-#{dossier.id}/export_#{dossier.id}.pdf" + expect(subject.first.second).to eq "DOSSIER-#{dossier.id}/export-#{dossier.id}.pdf" end end end From f973c59c9ac0e7eff2e9dced5bcfbd7fd9c0b003 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 23 Jul 2024 17:09:28 +0200 Subject: [PATCH 07/16] stop exporting bills with export_template --- app/services/pieces_justificatives_service.rb | 12 +++++------- spec/services/pieces_justificatives_service_spec.rb | 12 ++++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index e3972d67a..acdc9b459 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -5,25 +5,23 @@ class PiecesJustificativesService end def liste_documents(dossiers) - bill_ids = [] - docs = pjs_for_champs(dossiers) + pjs_for_commentaires(dossiers) + pjs_for_dossier(dossiers) + pjs_for_avis(dossiers) - if liste_documents_allows?(:with_bills) + # we do not export bills no more with the new export system + # the bills have never been properly understood by the users + # their export is now deprecated + if liste_documents_allows?(:with_bills) && @export_template.nil? # some bills are shared among operations # so first, all the bill_ids are fetched operation_logs, some_bill_ids = operation_logs_and_signature_ids(dossiers) docs += operation_logs - bill_ids += some_bill_ids - end - if liste_documents_allows?(:with_bills) # then the bills are retrieved without duplication - docs += signatures(bill_ids.uniq) + docs += signatures(some_bill_ids.uniq) end docs.filter { |_attachment, path| path.present? } diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index 900c12a05..9c999588c 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -312,6 +312,12 @@ describe PiecesJustificativesService do expect(subject).to match_array([dossier_bs.serialized.attachment, dossier_bs.signature.attachment]) end + context 'with export_template' do + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } + end + context 'with a dol' do let(:dol) { create(:dossier_operation_log, dossier: dossier) } let(:witness_dol) { create(:dossier_operation_log, dossier: witness) } @@ -322,6 +328,12 @@ describe PiecesJustificativesService do end it { expect(subject).to include(dol.serialized.attachment) } + + context 'with export_template' do + let(:export_template) { default_export_template } + + it { expect(subject).to be_empty } + end end end From 057868a48ed35c9219f16d7ab41e4fc213abf9dd Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:22:04 +0200 Subject: [PATCH 08/16] small work on tiptap --- app/models/export_item.rb | 4 +-- app/services/tiptap_service.rb | 24 ++++++++++------ spec/services/tiptap_service_spec.rb | 41 +++++++++++++++++++++------- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/app/models/export_item.rb b/app/models/export_item.rb index 8ee6e4323..294946753 100644 --- a/app/models/export_item.rb +++ b/app/models/export_item.rb @@ -21,14 +21,14 @@ class ExportItem def template_json = template.to_json - def template_string = TiptapService.new.to_path(template) + def template_string = TiptapService.new.to_texts_and_tags(template) def path(dossier, attachment: nil, row_index: nil, index: nil) used_tags = TiptapService.used_tags_and_libelle_for(template) substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true) substitutions['original-filename'] = attachment.filename.base if attachment - TiptapService.new.to_path(template, substitutions) + suffix(attachment, row_index, index) + TiptapService.new.to_texts_and_tags(template, substitutions) + suffix(attachment, row_index, index) end def ==(other) diff --git a/app/services/tiptap_service.rb b/app/services/tiptap_service.rb index 3bbee3494..3b6e61d19 100644 --- a/app/services/tiptap_service.rb +++ b/app/services/tiptap_service.rb @@ -19,10 +19,10 @@ class TiptapService children(node[:content], substitutions, 0) end - def to_path(node, substitutions = {}) + def to_texts_and_tags(node, substitutions = {}) return '' if node.nil? - children_path(node[:content], substitutions) + children_texts_and_tags(node[:content], substitutions) end private @@ -31,18 +31,24 @@ class TiptapService @body_started = false end - def children_path(content, substitutions) - content.map { node_to_path(_1, substitutions) }.join + def children_texts_and_tags(content, substitutions) + content.map { node_to_texts_and_tags(_1, substitutions) }.join end - def node_to_path(node, substitutions) + def node_to_texts_and_tags(node, substitutions) case node in type: 'paragraph', content: - children_path(content, substitutions) - in type: 'text', text:, **rest + children_texts_and_tags(content, substitutions) + in type: 'paragraph' # empty paragraph + '' + in type: 'text', text: text.strip - in type: 'mention', attrs: { id: }, **rest - substitutions.fetch(id) { "--#{id}--" } + in type: 'mention', attrs: { id:, label: } + if substitutions.present? + substitutions.fetch(id) { "--#{id}--" } + else + "#{label}" + end end end diff --git a/spec/services/tiptap_service_spec.rb b/spec/services/tiptap_service_spec.rb index 1ec0468b2..365fcae3b 100644 --- a/spec/services/tiptap_service_spec.rb +++ b/spec/services/tiptap_service_spec.rb @@ -193,19 +193,40 @@ RSpec.describe TiptapService do end end - describe '.to_path' do - let(:substitutions) { { "dossier_number" => "42" } } - let(:json) do - { - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " .pdf", "type" => "text" }] } - ] + describe '.to_texts_and_tags' do + subject { described_class.new.to_texts_and_tags(json, substitutions) } - }.deep_symbolize_keys + context 'nominal' do + let(:json) do + { + "content" => [ + { "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " .pdf", "type" => "text" }] } + ] + + }.deep_symbolize_keys + end + + context 'with substitutions' do + let(:substitutions) { { "dossier_number" => "42" } } + it 'returns texts_and_tags' do + is_expected.to eq("export_42.pdf") + end + end + + context 'without substitutions' do + let(:substitutions) { nil } + + it 'returns texts_and_tags' do + is_expected.to eq("export_numéro du dossier.pdf") + end + end end - it 'returns path' do - expect(described_class.new.to_path(json, substitutions)).to eq("export_42.pdf") + context 'empty paragraph' do + let(:json) { { content: [{ type: 'paragraph' }] } } + let(:substitutions) { {} } + + it { is_expected.to eq('') } end end end From 248da3a896e1128a8e6a1c4d4e54885e133b2f78 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:23:14 +0200 Subject: [PATCH 09/16] work on validator --- app/validators/export_template_validator.rb | 82 +++++++++++-------- config/locales/models/export_templates/en.yml | 2 + config/locales/models/export_templates/fr.yml | 2 + .../export_template_validator_spec.rb | 79 ++++++++++++++++++ 4 files changed, 130 insertions(+), 35 deletions(-) create mode 100644 spec/validators/export_template_validator_spec.rb diff --git a/app/validators/export_template_validator.rb b/app/validators/export_template_validator.rb index 2a1158ed6..6d291ea3d 100644 --- a/app/validators/export_template_validator.rb +++ b/app/validators/export_template_validator.rb @@ -1,54 +1,66 @@ class ExportTemplateValidator < ActiveModel::Validator - def validate(record) - validate_default_dossier_directory(record) - validate_pdf_name(record) - validate_pjs(record) + def validate(export_template) + validate_all_templates(export_template) + + return if export_template.errors.any? # no need to continue if the templates are invalid + + validate_dossier_folder(export_template) + validate_export_pdf(export_template) + validate_pjs(export_template) + + validate_different_templates(export_template) end private - def validate_default_dossier_directory(record) - mention = attribute_content_mention(record, :default_dossier_directory) - if mention&.fetch("id", nil) != "dossier_number" - record.errors.add :tiptap_default_dossier_directory, :dossier_number_mandatory + def validate_all_templates(export_template) + [export_template.dossier_folder, export_template.export_pdf, *export_template.pjs].each(&:template_string) + + rescue StandardError + export_template.errors.add(:base, :invalid_template) + end + + def validate_dossier_folder(export_template) + if !mentions(export_template.dossier_folder.template).include?('dossier_number') + export_template.errors.add(:dossier_folder, :dossier_number_required) end end - def validate_pdf_name(record) - if attribute_content_text(record, :pdf_name).blank? && attribute_content_mention(record, :pdf_name).blank? - record.errors.add :tiptap_pdf_name, :blank + def mentions(template) + TiptapService.used_tags_and_libelle_for(template).map(&:first) + end + + def validate_export_pdf(export_template) + return if !export_template.export_pdf.enabled? + + if export_template.export_pdf.template_string.empty? + export_template.errors.add(:export_pdf, :blank) end end - def attribute_content_text(record, attribute) - attribute_content(record, attribute)&.find { |elem| elem["type"] == "text" }&.fetch("text", nil) - end + def validate_pjs(export_template) + libelle_by_stable_ids = pj_libelle_by_stable_id(export_template) - def attribute_content_mention(record, attribute) - attribute_content(record, attribute)&.find { |elem| elem["type"] == "mention" }&.fetch("attrs", nil) - end - - def attribute_content(record, attribute) - content = record.content[attribute.to_s]&.fetch("content", nil) - if content.is_a?(Array) - content.first&.fetch("content", nil) + export_template.pjs.filter(&:enabled?).each do |pj| + if pj.template_string.empty? + libelle = libelle_by_stable_ids[pj.stable_id] + export_template.errors.add(libelle, I18n.t(:blank, scope: 'errors.messages')) + end end end - def validate_pjs(record) - record.content["pjs"]&.each do |pj| - pj_sym = pj.symbolize_keys - libelle = record.groupe_instructeur.procedure.exportables_pieces_jointes.find { _1.stable_id.to_s == pj_sym[:stable_id] }&.libelle&.to_sym - validate_content(record, pj_sym[:path], libelle) - end + def validate_different_templates(export_template) + templates = [export_template.export_pdf, *export_template.pjs] + .filter(&:enabled?) + .map(&:template_string) + + return if templates.uniq.size == templates.size + + export_template.errors.add(:base, :different_templates) end - def validate_content(record, attribute_content, attribute) - if attribute_content.nil? || attribute_content["content"].nil? || - attribute_content["content"].first.nil? || - attribute_content["content"].first["content"].nil? || - (attribute_content["content"].first["content"].find { |elem| elem["text"].blank? } && attribute_content["content"].first["content"].find { |elem| elem["type"] == "mention" }["attrs"].blank?) - record.errors.add attribute, I18n.t(:blank, scope: 'errors.messages') - end + def pj_libelle_by_stable_id(export_template) + export_template.procedure.exportables_pieces_jointes + .pluck(:stable_id, :libelle).to_h end end diff --git a/config/locales/models/export_templates/en.yml b/config/locales/models/export_templates/en.yml index e6cd08856..9ec65402c 100644 --- a/config/locales/models/export_templates/en.yml +++ b/config/locales/models/export_templates/en.yml @@ -15,3 +15,5 @@ en: models: export_template: dossier_number_mandatory: "must contain dossier's number" + different_templates: "Files must have different names" + invalid_template: "A file name is invalid" diff --git a/config/locales/models/export_templates/fr.yml b/config/locales/models/export_templates/fr.yml index 60852aee0..04ef38215 100644 --- a/config/locales/models/export_templates/fr.yml +++ b/config/locales/models/export_templates/fr.yml @@ -15,3 +15,5 @@ fr: models: export_template: dossier_number_mandatory: doit contenir le numéro du dossier + different_templates: Les fichiers doivent avoir des noms différents + invalid_template: Un nom de fichier est invalide diff --git a/spec/validators/export_template_validator_spec.rb b/spec/validators/export_template_validator_spec.rb new file mode 100644 index 000000000..53e1c7f0a --- /dev/null +++ b/spec/validators/export_template_validator_spec.rb @@ -0,0 +1,79 @@ +describe ExportTemplateValidator do + let(:validator) { ExportTemplateValidator.new } + + describe 'validate' do + let(:exportables_pieces_jointes) { [double('pj', stable_id: 3, libelle: 'libelle')] } + let(:pj_libelle_by_stable_id) { exportables_pieces_jointes.map { |pj| [pj.stable_id, pj.libelle] }.to_h } + + def empty_template(enabled: true, stable_id: nil) + { template: { type: "doc", content: [] }, enabled: enabled, stable_id: stable_id }.compact + end + + def errors(export_template) = export_template.errors.map { [_1.attribute, _1.message] } + + before do + allow(validator).to receive(:pj_libelle_by_stable_id).and_return(pj_libelle_by_stable_id) + validator.validate(export_template) + end + + context 'with a default export template' do + let(:export_template) { build(:export_template) } + + it { expect(export_template.errors.count).to eq(0) } + end + + context 'with a invalid template' do + let(:export_template) do + export_pdf = { template: { is: 'invalid' }, enabled: true } + build(:export_template, export_pdf:) + end + + it { expect(errors(export_template)).to eq([[:base, "Un nom de fichier est invalide"]]) } + end + + context 'with a empty export_pdf' do + let(:export_template) { build(:export_template, export_pdf: empty_template) } + + it { expect(errors(export_template)).to eq([[:export_pdf, "doit être rempli"]]) } + end + + context 'with a empty export_pdf disabled' do + let(:export_template) { build(:export_template, export_pdf: empty_template(enabled: false)) } + + it { expect(export_template.errors.count).to eq(0) } + end + + context 'with a dossier_folder without dossier_number' do + let(:export_template) do + dossier_folder = ExportItem.default(prefix: 'dossier') + dossier_folder.template[:content][0][:content][1][:attrs][:id] = :other + + build(:export_template, dossier_folder:) + end + + it { expect(errors(export_template)).to eq([[:dossier_folder, "doit contenir le numéro du dossier"]]) } + end + + context 'with a empty pj' do + let(:export_template) { build(:export_template, pjs: [empty_template(stable_id: 3)]) } + + it { expect(errors(export_template)).to eq([[:libelle, "doit être rempli"]]) } + end + + context 'with a empty pj disabled' do + let(:export_template) { build(:export_template, pjs: [empty_template(enabled: false)]) } + + it { expect(export_template.errors.count).to eq(0) } + end + + context 'with multiple files bearing the same template' do + let(:export_item) { ExportItem.default(prefix: 'same') } + + let(:export_template) do + build(:export_template, export_pdf: export_item, pjs: [export_item]) + end + + it { expect(errors(export_template)).to eq([[:base, "Les fichiers doivent avoir des noms différents"]]) } + end + end +end From 30e79b735d70371587abfbd8847dd7435462fec3 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Thu, 18 Jul 2024 17:27:19 +0200 Subject: [PATCH 10/16] route: allow standard path helper to work for example `form_with model: [:instructeur, procedure, export_template]` --- .../export_dropdown_component.html.haml | 2 +- config/routes.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 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 fd29214f2..441149066 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 @@ -22,5 +22,5 @@ = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do = "Exporter à partir du modèle #{export_template.name}" - menu.with_item do - = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do + = link_to [:new, :instructeur, @procedure, :export_template], role: 'menuitem' do Ajouter un modèle d'export diff --git a/config/routes.rb b/config/routes.rb index 3a8e72fba..9c2239267 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -443,14 +443,17 @@ Rails.application.routes.draw do # scope module: 'instructeurs', as: 'instructeur' do + resources :procedures, only: [] do + resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do + collection do + get 'preview' + end + end + end + resources :procedures, only: [:index, :show], param: :procedure_id do member do resources :archives, only: [:index, :create] - resources :export_templates, only: [:new, :create, :edit, :update, :destroy] do - collection do - get 'preview' - end - end resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do resource :contact_information From c4403bffeb8fa2c435aaac3f89ce1cb5507b4bee Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:26:49 +0200 Subject: [PATCH 11/16] work on controller --- .../export_templates_controller.rb | 72 +++----- .../export_templates_controller_spec.rb | 159 +++++++++++------- 2 files changed, 119 insertions(+), 112 deletions(-) diff --git a/app/controllers/instructeurs/export_templates_controller.rb b/app/controllers/instructeurs/export_templates_controller.rb index 64ef44a4e..65a4c7dc5 100644 --- a/app/controllers/instructeurs/export_templates_controller.rb +++ b/app/controllers/instructeurs/export_templates_controller.rb @@ -1,21 +1,18 @@ module Instructeurs class ExportTemplatesController < InstructeurController - before_action :set_procedure - before_action :set_groupe_instructeur, only: [:create, :update] + before_action :set_procedure_and_groupe_instructeurs before_action :set_export_template, only: [:edit, :update, :destroy] - before_action :set_groupe_instructeurs - before_action :set_all_pj + before_action :ensure_legitimate_groupe_instructeur, only: [:create, :update] def new - @export_template = ExportTemplate.new(kind: 'zip', groupe_instructeur: @groupe_instructeurs.first) - @export_template.set_default_values + @export_template = ExportTemplate.default(groupe_instructeur: @groupe_instructeurs.first) end def create - @export_template = @groupe_instructeur.export_templates.build(export_template_params) - @export_template.assign_pj_names(pj_params) + @export_template = ExportTemplate.new(export_template_params) + if @export_template.save - redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été créé" + redirect_to [:exports, :instructeur, @procedure], notice: "Le modèle d'export #{@export_template.name} a bien été créé" else flash[:alert] = @export_template.errors.full_messages render :new @@ -26,11 +23,8 @@ module Instructeurs end def update - @export_template.assign_attributes(export_template_params) - @export_template.groupe_instructeur = @groupe_instructeur - @export_template.assign_pj_names(pj_params) - if @export_template.save - redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été modifié" + if @export_template.update(export_template_params) + redirect_to [:exports, :instructeur, @procedure], notice: "Le modèle d'export #{@export_template.name} a bien été modifié" else flash[:alert] = @export_template.errors.full_messages render :edit @@ -39,62 +33,40 @@ module Instructeurs def destroy if @export_template.destroy - redirect_to exports_instructeur_procedure_path(procedure: @procedure), notice: "Le modèle d'export #{@export_template.name} a bien été supprimé" + redirect_to [:exports, :instructeur, @procedure], notice: "Le modèle d'export #{@export_template.name} a bien été supprimé" else - redirect_to exports_instructeur_procedure_path(procedure: @procedure), alert: "Le modèle d'export #{@export_template.name} n'a pu être supprimé" + redirect_to [:exports, :instructeur, @procedure], alert: "Le modèle d'export #{@export_template.name} n'a pu être supprimé" end end def preview - set_groupe_instructeur - @export_template = @groupe_instructeur.export_templates.build(export_template_params) - @export_template.assign_pj_names(pj_params) + export_template = ExportTemplate.new(export_template_params) - @sample_dossier = @procedure.dossier_for_preview(current_instructeur) - - render turbo_stream: turbo_stream.replace('preview', partial: 'preview', locals: { export_template: @export_template, procedure: @procedure, dossier: @sample_dossier }) + render turbo_stream: turbo_stream.replace('preview', partial: 'preview', locals: { export_template: }) end private def export_template_params - params.require(:export_template).permit(*export_params) + params.require(:export_template) + .permit(:name, :kind, :groupe_instructeur_id, dossier_folder: [:enabled, :template], export_pdf: [:enabled, :template], pjs: [:stable_id, :enabled, :template]) end - def set_procedure - @procedure = current_instructeur.procedures.find params[:procedure_id] - Sentry.configure_scope do |scope| - scope.set_tags(procedure: @procedure.id) - end + def set_procedure_and_groupe_instructeurs + @procedure = current_instructeur.procedures.find(params[:procedure_id]) + @groupe_instructeurs = current_instructeur.groupe_instructeurs.where(procedure: @procedure) + + Sentry.configure_scope { |scope| scope.set_tags(procedure: @procedure.id) } end def set_export_template @export_template = current_instructeur.export_templates.find(params[:id]) end - def set_groupe_instructeur - @groupe_instructeur = @procedure.groupe_instructeurs.find(params.require(:export_template)[:groupe_instructeur_id]) - end + def ensure_legitimate_groupe_instructeur + return if export_template_params[:groupe_instructeur_id].in?(@groupe_instructeurs.map { _1.id.to_s }) - def set_groupe_instructeurs - @groupe_instructeurs = current_instructeur.groupe_instructeurs.where(procedure: @procedure) - end - - def set_all_pj - @all_pj ||= @procedure.exportables_pieces_jointes - end - - def export_params - [:name, :kind, :tiptap_default_dossier_directory, :tiptap_pdf_name] - end - - def pj_params - @procedure = current_instructeur.procedures.find params[:procedure_id] - pj_params = [] - @all_pj.each do |pj| - pj_params << "tiptap_pj_#{pj.stable_id}".to_sym - end - params.require(:export_template).permit(*pj_params) + redirect_to [:exports, :instructeur, @procedure], alert: 'Vous n’avez pas le droit de créer un modèle d’export pour ce groupe' end end end diff --git a/spec/controllers/instructeurs/export_templates_controller_spec.rb b/spec/controllers/instructeurs/export_templates_controller_spec.rb index 36c9f665e..df19aa1b4 100644 --- a/spec/controllers/instructeurs/export_templates_controller_spec.rb +++ b/spec/controllers/instructeurs/export_templates_controller_spec.rb @@ -1,58 +1,31 @@ describe Instructeurs::ExportTemplatesController, type: :controller do before { sign_in(instructeur.user) } - let(:tiptap_pdf_name) { - { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "mon_export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } - ] - }.to_json - } - - let(:export_template_params) do - { - name: "coucou", - kind: "zip", - groupe_instructeur_id: groupe_instructeur.id, - tiptap_pdf_name: tiptap_pdf_name, - tiptap_default_dossier_directory: { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] } - ] - }.to_json, - tiptap_pj_3: { - "type" => "doc", - "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }] - }.to_json, - tiptap_pj_5: { - - "type" => "doc", - "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }] - }.to_json, - tiptap_pj_10: { - - "type" => "doc", - "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }] - }.to_json - } - end let(:instructeur) { create(:instructeur) } let(:procedure) do create( :procedure, instructeurs: [instructeur], - types_de_champ_public: [ - { type: :piece_justificative, libelle: "pj1", stable_id: 3 }, - { type: :piece_justificative, libelle: "pj2", stable_id: 5 }, - { type: :piece_justificative, libelle: "pj3", stable_id: 10 } - ] + types_de_champ_public: [{ type: :piece_justificative, libelle: "pj1", stable_id: 3 }] ) end let(:groupe_instructeur) { procedure.defaut_groupe_instructeur } + let(:groupe_instructeur_id) { groupe_instructeur.id } + + let(:export_template_params) do + { + name: "coucou", + kind: "zip", + groupe_instructeur_id:, + export_pdf:, + dossier_folder: item_params(text: "DOSSIER_"), + pjs: [pj_item_params(stable_id: 3, text: "avis-commission-"), pj_item_params(stable_id: 666, text: "evil-hack")] + } + end + + let(:export_pdf) { item_params(text: "mon_export_") } describe '#new' do - let(:subject) { get :new, params: { procedure_id: procedure.id } } + subject { get :new, params: { procedure_id: procedure.id } } it do subject @@ -61,18 +34,22 @@ describe Instructeurs::ExportTemplatesController, type: :controller do end describe '#create' do - let(:subject) { post :create, params: { procedure_id: procedure.id, export_template: export_template_params } } + let(:create_params) { export_template_params } + subject { post :create, params: { procedure_id: procedure.id, export_template: create_params } } context 'with valid params' do it 'redirect to some page' do subject - expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:)) + expect(response).to redirect_to(exports_instructeur_procedure_path(procedure)) expect(flash.notice).to eq "Le modèle d'export coucou a bien été créé" end end context 'with invalid params' do - let(:tiptap_pdf_name) { { content: "invalid" }.to_json } + let(:export_pdf) do + item_params(text: 'toto').merge("template" => { "content" => [{ "content" => "invalid" }] }.to_json) + end + it 'display error notification' do subject expect(flash.alert).to be_present @@ -81,16 +58,37 @@ describe Instructeurs::ExportTemplatesController, type: :controller do context 'with procedure not accessible by current instructeur' do let(:another_procedure) { create(:procedure) } - let(:subject) { post :create, params: { procedure_id: another_procedure.id, export_template: export_template_params } } + subject { post :create, params: { procedure_id: another_procedure.id, export_template: export_template_params } } + it 'raise exception' do expect { subject }.to raise_error(ActiveRecord::RecordNotFound) end end + + context 'with invalid groupe_instructeur_id' do + let(:groupe_instructeur_id) { create(:groupe_instructeur).id } + + it 'display error notification' do + expect { subject }.not_to change(ExportTemplate, :count) + expect(flash.alert).to be_present + end + end + + context 'without pjs' do + let(:create_params) { export_template_params.tap { _1.delete(:pjs) } } + + it 'works' do + subject + + expect(flash.notice).to eq "Le modèle d'export coucou a bien été créé" + expect(ExportTemplate.last.pjs).to match_array([]) + end + end end describe '#edit' do let(:export_template) { create(:export_template, groupe_instructeur:) } - let(:subject) { get :edit, params: { procedure_id: procedure.id, id: export_template.id } } + subject { get :edit, params: { procedure_id: procedure.id, id: export_template.id } } it 'render edit' do subject @@ -109,42 +107,53 @@ describe Instructeurs::ExportTemplatesController, type: :controller do describe '#update' do let(:export_template) { create(:export_template, groupe_instructeur:) } - let(:tiptap_pdf_name) { - { - "type" => "doc", - "content" => [ - { "type" => "paragraph", "content" => [{ "text" => "exPort_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] } - ] - }.to_json - } + let(:export_pdf) { item_params(text: "exPort_") } - let(:subject) { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } } + subject { put :update, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params } } context 'with valid params' do it 'redirect to some page' do subject - expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:)) + expect(response).to redirect_to(exports_instructeur_procedure_path(procedure)) expect(flash.notice).to eq "Le modèle d'export coucou a bien été modifié" + + export_template.reload + + expect(export_template.export_pdf.template_json).to eq(item_params(text: "exPort_")["template"]) + expect(export_template.pjs.map(&:template_json)).to eq([item_params(text: "avis-commission-")["template"]]) end end context 'with invalid params' do - let(:tiptap_pdf_name) { { content: "invalid" }.to_json } + let(:export_pdf) do + item_params(text: 'a').merge("template" => { "content" => [{ "content" => "invalid" }] }.to_json) + end + it 'display error notification' do subject expect(flash.alert).to be_present end end + + context 'with invalid groupe_instructeur_id' do + let(:groupe_instructeur_id) { create(:groupe_instructeur).id } + + it 'display error notification' do + subject + expect(export_template.export_pdf.template_json).not_to eq(item_params(text: "exPort_")["template"]) + expect(flash.alert).to be_present + end + end end describe '#destroy' do let(:export_template) { create(:export_template, groupe_instructeur:) } - let(:subject) { delete :destroy, params: { procedure_id: procedure.id, id: export_template.id } } + subject { delete :destroy, params: { procedure_id: procedure.id, id: export_template.id } } context 'with valid params' do it 'redirect to some page' do subject - expect(response).to redirect_to(exports_instructeur_procedure_path(procedure:)) + expect(response).to redirect_to(exports_instructeur_procedure_path(procedure)) expect(flash.notice).to eq "Le modèle d'export Mon export a bien été supprimé" end end @@ -155,7 +164,7 @@ describe Instructeurs::ExportTemplatesController, type: :controller do let(:export_template) { create(:export_template, groupe_instructeur:) } - let(:subject) { get :preview, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params }, format: :turbo_stream } + subject { get :preview, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params }, format: :turbo_stream } it '' do dossier = create(:dossier, procedure: procedure, for_procedure_preview: true) @@ -164,4 +173,30 @@ describe Instructeurs::ExportTemplatesController, type: :controller do expect(response.body).to include "mon_export_#{dossier.id}.pdf" end end + + def pj_item_params(stable_id:, text:, enabled: true) + item_params(text: text, enabled: enabled).merge("stable_id" => stable_id.to_s) + end + + def item_params(text:, enabled: true) + { + "enabled" => enabled, + "template" => { + "type" => "doc", + "content" => content(text:) + }.to_json + } + end + + def content(text:) + [ + { + "type" => "paragraph", + "content" => [ + { "text" => text, "type" => "text" }, + { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } } + ] + } + ] + end end From 393db312c2a5dd257dd5bab9023315173b54c4e3 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:28:02 +0200 Subject: [PATCH 12/16] a little js --- .../controllers/hide_target_controller.ts | 19 ++++++++++++++++++ .../controllers/lazy/tiptap_controller.ts | 5 ++++- .../tiptap_to_template_controller.ts | 20 +++++++++++++++++++ app/javascript/shared/tiptap/editor.ts | 15 +++++++++++--- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 app/javascript/controllers/hide_target_controller.ts create mode 100644 app/javascript/controllers/tiptap_to_template_controller.ts diff --git a/app/javascript/controllers/hide_target_controller.ts b/app/javascript/controllers/hide_target_controller.ts new file mode 100644 index 000000000..607543aac --- /dev/null +++ b/app/javascript/controllers/hide_target_controller.ts @@ -0,0 +1,19 @@ +import { Controller } from '@hotwired/stimulus'; + +export class HideTargetController extends Controller { + static targets = ['source', 'toHide']; + declare readonly toHideTargets: HTMLDivElement[]; + declare readonly sourceTargets: HTMLInputElement[]; + + connect() { + this.sourceTargets.forEach((source) => { + source.addEventListener('click', this.handleInput.bind(this)); + }); + } + + handleInput() { + this.toHideTargets.forEach((toHide) => { + toHide.classList.toggle('fr-hidden'); + }); + } +} diff --git a/app/javascript/controllers/lazy/tiptap_controller.ts b/app/javascript/controllers/lazy/tiptap_controller.ts index 58336e926..eeb2a58bb 100644 --- a/app/javascript/controllers/lazy/tiptap_controller.ts +++ b/app/javascript/controllers/lazy/tiptap_controller.ts @@ -10,7 +10,8 @@ import { createEditor } from '../../shared/tiptap/editor'; export class TiptapController extends ApplicationController { static targets = ['editor', 'input', 'button', 'tag']; static values = { - insertAfterTag: { type: String, default: '' } + insertAfterTag: { type: String, default: '' }, + attributes: { type: Object, default: {} } }; declare editorTarget: Element; @@ -18,6 +19,7 @@ export class TiptapController extends ApplicationController { declare buttonTargets: HTMLButtonElement[]; declare tagTargets: HTMLElement[]; declare insertAfterTagValue: string; + declare attributesValue: Record; #initializing = true; #editor?: Editor; @@ -28,6 +30,7 @@ export class TiptapController extends ApplicationController { content: this.content, tags: this.tags, buttons: this.menuButtons, + attributes: { class: 'fr-input', ...this.attributesValue }, onChange: ({ editor }) => { for (const button of this.buttonTargets) { const action = getAction(editor, button); diff --git a/app/javascript/controllers/tiptap_to_template_controller.ts b/app/javascript/controllers/tiptap_to_template_controller.ts new file mode 100644 index 000000000..90f58294e --- /dev/null +++ b/app/javascript/controllers/tiptap_to_template_controller.ts @@ -0,0 +1,20 @@ +import { Controller } from '@hotwired/stimulus'; + +export class TiptapToTemplateController extends Controller { + static targets = ['output', 'trigger']; + + declare readonly outputTarget: HTMLElement; + declare readonly triggerTarget: HTMLButtonElement; + + connect() { + this.triggerTarget.addEventListener('click', this.handleClick.bind(this)); + } + + handleClick() { + const template = this.element.querySelector('.tiptap.ProseMirror p'); + + if (template) { + this.outputTarget.innerHTML = template.innerHTML; + } + } +} diff --git a/app/javascript/shared/tiptap/editor.ts b/app/javascript/shared/tiptap/editor.ts index 3c12fc792..590aa0701 100644 --- a/app/javascript/shared/tiptap/editor.ts +++ b/app/javascript/shared/tiptap/editor.ts @@ -33,6 +33,7 @@ export function createEditor({ content, tags, buttons, + attributes, onChange }: { editorElement: Element; @@ -40,8 +41,15 @@ export function createEditor({ tags: TagSchema[]; buttons: string[]; onChange(change: { editor: Editor }): void; + attributes?: Record; }): Editor { - const options = getEditorOptions(editorElement, tags, buttons, content); + const options = getEditorOptions( + editorElement, + tags, + buttons, + content, + attributes + ); const editor = new Editor(options); editor.on('transaction', onChange); return editor; @@ -51,7 +59,8 @@ function getEditorOptions( element: Element, tags: TagSchema[], actions: string[], - content?: JSONContent + content?: JSONContent, + attributes?: Record ): Partial { const extensions: Extensions = []; for (const action of actions) { @@ -123,7 +132,7 @@ function getEditorOptions( return { element, content, - editorProps: { attributes: { class: 'fr-input' } }, + editorProps: { attributes }, extensions: [ actions.includes('title') ? DocumentWithHeader : Document, Hystory, From 57f698853f7a43586e0ec125ffef1e25b4af455c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:28:36 +0200 Subject: [PATCH 13/16] some layouts --- app/helpers/application_helper.rb | 2 + .../export_templates/_export_item.html.haml | 26 +++++ .../export_templates/_form.html.haml | 108 ++++++++---------- .../export_templates/_preview.html.haml | 46 +++++--- .../export_templates/edit.html.haml | 2 +- .../export_templates/new.html.haml | 2 +- .../instructeurs/procedures/exports.html.haml | 4 +- 7 files changed, 113 insertions(+), 77 deletions(-) create mode 100644 app/views/instructeurs/export_templates/_export_item.html.haml diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 856cc923e..177d5196e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -150,4 +150,6 @@ module ApplicationHelper .map { |word| word[0].upcase } .join end + + def asterisk = render(EditableChamp::AsteriskMandatoryComponent.new) end diff --git a/app/views/instructeurs/export_templates/_export_item.html.haml b/app/views/instructeurs/export_templates/_export_item.html.haml new file mode 100644 index 000000000..6ab854adf --- /dev/null +++ b/app/views/instructeurs/export_templates/_export_item.html.haml @@ -0,0 +1,26 @@ +.card.no-list + = hidden_field_tag("#{prefix}[stable_id]", item.stable_id) + + .fr-checkbox-group{ data: { controller: 'hide-target' } } + - id = sanitize_to_id("#{prefix}_#{item.stable_id}_enabled") + = check_box_tag "#{prefix}[enabled]", true, item.enabled?, id:, data: { 'hide-target_target': 'source' } + = label_tag id, libelle, class: 'fr-label' + + %div{ class: class_names('fr-hidden': !item.enabled?), data: { hide_target_target: 'toHide' } } + %div{ data: { controller: 'hide-target tiptap-to-template'} } + .fr-mt-2w{ data: { hide_target_target: 'toHide' } } + %span Nom du fichier : + %span{ data: { 'tiptap-to-template_target': 'output'} }= sanitize(item.template_string) + .fr-mt-2w + %button.fr-btn.fr-btn--tertiary.fr-btn--sm{ type: 'button', data: { 'hide-target_target': 'source' } } Renommer le fichier + + .fr-mt-2w.fr-hidden{ data: { controller: 'tiptap', 'tiptap-attributes-value': { spellcheck: false }.to_json, hide_target_target: 'toHide' } } + %span Renommer le fichier : + .fr-mt-2w.tiptap-editor{ data: { tiptap_target: 'editor' } } + = hidden_field_tag "#{prefix}[template]", item.template_json, data: { tiptap_target: 'input' }, id: nil + + .fr-mt-2w + %span.fr-text--sm Cliquez sur les étiquettes que vous souhaitez intégrer au nom du fichier + .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.pj_tags }) + + = button_tag "Valider", type: 'button', class: 'fr-btn fr-mt-2w', data: { 'tiptap-to-template_target': 'trigger', 'hide-target_target': 'source'} diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 00813d9b6..1de02b429 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -1,3 +1,5 @@ +- procedure = @export_template.procedure + #export_template-edit.fr-my-4w .fr-mb-6w = render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur de modèle d'export", heading_level: 'h3') do |c| @@ -7,72 +9,62 @@ Essayez-le et donnez-nous votre avis en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Editeur de modèle d'export")}. -.fr-grid-row.fr-grid-row--gutters - .fr-col-12.fr-col-md-8 - = form_with url: form_url, model: @export_template, local: true, data: { turbo: 'true', controller: 'autosubmit' } do |f| + .fr-grid-row.fr-grid-row--gutters + .fr-col-12.fr-col-md-8.fr-pr-4w + = form_with model: [:instructeur, procedure, export_template], data: { turbo: 'true', controller: 'autosubmit' } do |f| + %input.hidden{ type: 'submit', formaction: preview_instructeur_procedure_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'get' } - = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) + = f.hidden_field :kind, value: 'zip' - - if groupe_instructeurs.many? - .fr-input-group + = render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field) + + .fr-input-group{ class: class_names('fr-hidden': groupe_instructeurs.one?) } = f.label :groupe_instructeur_id, class: 'fr-label' do - = f.object.class.human_attribute_name(:groupe_instructeur_id) - = render EditableChamp::AsteriskMandatoryComponent.new - %span.fr-hint-text - Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ? + = "#{ExportTemplate.human_attribute_name('groupe_instructeur_id')} #{asterisk}" + %span.fr-hint-text Avec quel groupe instructeur souhaitez-vous partager ce modèle d'export ? = f.collection_select :groupe_instructeur_id, groupe_instructeurs, :id, :label, {}, class: 'fr-select' - - else - = f.hidden_field :groupe_instructeur_id - = f.hidden_field :kind + .fr-input-group{ data: { controller: 'tiptap', 'tiptap-attributes-value': { spellcheck: false }.to_json } } + = f.label '[dossier_folder][template]', class: "fr-label" do + = "#{ExportTemplate.human_attribute_name('dossier_folder')} #{asterisk}" + %span.fr-hint-text Nom du répertoire contenant les différents fichiers à exporter + .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } + = f.hidden_field "[dossier_folder][template]", data: { tiptap_target: 'input' }, value: export_template.dossier_folder.template_json + = f.hidden_field "[dossier_folder][enabled]", value: 'true' + .fr-mt-2w + %span.fr-text--sm Cliquez sur les étiquettes que vous souhaitez intégrer au nom du fichier + .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => export_template.tags }) - .fr-input-group{ data: { controller: 'tiptap' } } - = f.label :tiptap_default_dossier_directory, class: "fr-label" do - = f.object.class.human_attribute_name(:tiptap_default_dossier_directory) - = render EditableChamp::AsteriskMandatoryComponent.new - %span.fr-hint-text - = t('activerecord.attributes.export_template.hints.tiptap_default_dossier_directory') + = render Dsfr::NoticeComponent.new(data_attributes: { class: 'fr-my-4w' }) do |c| + - c.with_title do + Sélectionnez les fichiers que vous souhaitez exporter - .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } - = f.hidden_field :tiptap_default_dossier_directory, data: { tiptap_target: 'input' } - .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) + %h3 Dossier au format PDF + = render partial: 'export_item', + locals: { item: export_template.export_pdf, + libelle: ExportTemplate.human_attribute_name(:export_pdf), + prefix: 'export_template[export_pdf]' } - .fr-input-group{ data: { controller: 'tiptap' } } - = f.label :tiptap_pdf_name, class: "fr-label" do - = f.object.class.human_attribute_name(:tiptap_pdf_name) - = render EditableChamp::AsteriskMandatoryComponent.new - %span.fr-hint-text - = t('activerecord.attributes.export_template.hints.tiptap_pdf_name') - .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } - = f.hidden_field :tiptap_pdf_name, data: { tiptap_target: 'input' } - .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.specific_tags }) + %h3 Pièces justificatives + - procedure.exportables_pieces_jointes.each do |tdc| + - item = export_template.pj(tdc) + = render partial: 'export_item', + locals: { item:, + libelle: tdc.libelle, + prefix: 'export_template[pjs][]'} - - if @all_pj.any? - %h3 Pieces justificatives + .fixed-footer + .fr-container + %ul.fr-btns-group.fr-btns-group--inline-md + %li= f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' } + %li= link_to "Annuler", [:exports, :instructeur, procedure], class: "fr-btn fr-btn--secondary" + - if export_template.persisted? + %li + = link_to "Supprimer", + [:instructeur, procedure, export_template], + method: :delete, + data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"}, + class: "fr-btn fr-btn--secondary" - .fr-highlight - %p.fr-text--sm - N'incluez pas les extensions de fichier (.pdf, .jpg, …) dans les noms de pièces jointes. - - - @all_pj.each do |pj| - .fr-input-group{ data: { controller: 'tiptap' } } - = label_tag pj.libelle, nil, name: field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), class: "fr-label" - .tiptap-editor.fr-mt-1w{ data: { tiptap_target: 'editor' } } - = hidden_field_tag field_name(:export_template, "tiptap_pj_#{pj.stable_id}"), "#{@export_template.content_for_pj(pj)}" , data: { tiptap_target: 'input' } - .fr-mt-2w= render TagsButtonListComponent.new(tags: { nil => @export_template.tags_for_pj }) - - .fixed-footer - .fr-container - %ul.fr-btns-group.fr-btns-group--inline-md - %li - %input.hidden{ type: 'submit', formaction: preview_instructeur_export_templates_path, data: { autosubmit_target: 'submitter' }, formnovalidate: 'true', formmethod: 'get' } - = f.button "Enregistrer", class: "fr-btn", data: { turbo: 'false' } - %li - = link_to "Annuler", instructeur_procedure_path(@procedure), class: "fr-btn fr-btn--secondary" - - if @export_template.persisted? - %li - = link_to "Supprimer", instructeur_export_template_path(@export_template, procedure_id: @procedure.id), method: :delete, data: { confirm: "Voulez-vous vraiment supprimer ce modèle ? Il sera supprimé pour tous les instructeurs du groupe"}, class: "fr-btn fr-btn--secondary" - - sample_dossier = @procedure.dossier_for_preview(current_instructeur) - - if sample_dossier .fr-col-12.fr-col-md-4.fr-background-alt--blue-france - = render partial: 'preview', locals: { dossier: sample_dossier, export_template: @export_template, procedure: @procedure } + = render partial: 'preview', locals: { export_template: } diff --git a/app/views/instructeurs/export_templates/_preview.html.haml b/app/views/instructeurs/export_templates/_preview.html.haml index 124abc2c1..0df0548e0 100644 --- a/app/views/instructeurs/export_templates/_preview.html.haml +++ b/app/views/instructeurs/export_templates/_preview.html.haml @@ -1,17 +1,33 @@ +- procedure = export_template.procedure +- dossier = procedure.dossier_for_preview(current_instructeur) + #preview.export-template-preview.fr-p-2w.sticky--top %h2.fr-h4 Aperçu - %ul.tree.fr-text--sm - %li #{DownloadableFileService::EXPORT_DIRNAME}/ - %li - %ul - %li - %span#preview_default_dossier_directory #{export_template.tiptap_convert(dossier, "default_dossier_directory")}/ - %ul - %li#preview_pdf_name #{export_template.tiptap_convert(dossier, "pdf_name")}.pdf - - procedure.exportables_pieces_jointes.each do |pj| - %li{ id: "preview_pj_#{pj.stable_id}" } #{export_template.tiptap_convert_pj(dossier, pj.stable_id)}-1.jpg - %ul - %li - %span messagerie/ - %ul - %li un-autre-fichier.png + - if dossier.nil? + %p.fr-text--sm + Pour générer un aperçu fidèle avec tous les champs et les dates, + = link_to 'créez-vous un dossier', commencer_url(procedure.path), target: '_blank' + et acceptez-le : l’aperçu l’utilisera. + + - else + %ul.tree.fr-text--sm + %li + %span.fr-icon-folder-zip-line + #{DownloadableFileService::EXPORT_DIRNAME}/ + %li + %ul + %li + %span.fr-icon-folder-line + #{export_template.dossier_folder.path(dossier)}/ + %ul + - if export_template.export_pdf.enabled? + %li + %span.fr-icon-pdf-2-line + #{export_template.export_pdf.path(dossier)}.pdf + + - procedure.exportables_pieces_jointes.each do |tdc| + - export_pj = export_template.pj(tdc) + - if export_pj.enabled? + %li + %span.fr-icon-file-image-line + #{export_pj.path(dossier)}-1.jpg diff --git a/app/views/instructeurs/export_templates/edit.html.haml b/app/views/instructeurs/export_templates/edit.html.haml index bd4fc02b4..ab4c5f8c7 100644 --- a/app/views/instructeurs/export_templates/edit.html.haml +++ b/app/views/instructeurs/export_templates/edit.html.haml @@ -4,4 +4,4 @@ .fr-container %h1 Mise à jour modèle d'export - = render partial: 'form', locals: { form_url: instructeur_export_template_path(@procedure, @export_template), groupe_instructeurs: @groupe_instructeurs } + = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs } diff --git a/app/views/instructeurs/export_templates/new.html.haml b/app/views/instructeurs/export_templates/new.html.haml index eeff6baa9..10962b1cd 100644 --- a/app/views/instructeurs/export_templates/new.html.haml +++ b/app/views/instructeurs/export_templates/new.html.haml @@ -3,4 +3,4 @@ [t('.title')]] } .fr-container %h1 Nouveau modèle d'export - = render partial: 'form', locals: { form_url: instructeur_export_templates_path, groupe_instructeurs: @groupe_instructeurs } + = render partial: 'form', locals: { export_template: @export_template, groupe_instructeurs: @groupe_instructeurs } diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml index 793a7d960..e63eb4472 100644 --- a/app/views/instructeurs/procedures/exports.html.haml +++ b/app/views/instructeurs/procedures/exports.html.haml @@ -38,9 +38,9 @@ %tbody - @export_templates.each do |export_template| %tr - %td= link_to export_template.name, edit_instructeur_export_template_path(export_template, procedure_id: @procedure.id) + %td= link_to export_template.name, [:edit, :instructeur, @procedure, export_template] %td= export_template.groupe_instructeur.label if @procedure.groupe_instructeurs.many? %p - = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do + = link_to [:new, :instructeur, @procedure, :export_template], class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line' do Ajouter un modèle d'export From e9abdcbbf158558f93e2530330a75c94e7658998 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:29:18 +0200 Subject: [PATCH 14/16] make it beautiful --- app/assets/stylesheets/card.scss | 1 + app/assets/stylesheets/icons.scss | 32 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/app/assets/stylesheets/card.scss b/app/assets/stylesheets/card.scss index 82b6f7cf1..809d4ce88 100644 --- a/app/assets/stylesheets/card.scss +++ b/app/assets/stylesheets/card.scss @@ -51,6 +51,7 @@ &.no-list { ul { list-style: none !important; + padding-left: 0; } } diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index b363a9172..76f02d614 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -175,6 +175,30 @@ } } + &-file-image-line { + &:before, + &:after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z'%3E%3C/path%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z'%3E%3C/path%3E%3C/svg%3E"); + } + } + + &-folder-line { + &:before, + &:after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z'%3E%3C/path%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z'%3E%3C/path%3E%3C/svg%3E"); + } + } + + &-folder-zip-line { + &:before, + &:after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10.4142 3L12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142ZM18 18H14V15H16V13H14V11H16V9H14V7H11.5858L9.58579 5H4V19H20V7H16V9H18V11H16V13H18V18Z'%3E%3C/path%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10.4142 3L12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142ZM18 18H14V15H16V13H14V11H16V9H14V7H11.5858L9.58579 5H4V19H20V7H16V9H18V11H16V13H18V18Z'%3E%3C/path%3E%3C/svg%3E"); + } + } + &-intermediate-circle-fill { &:before, &:after { @@ -191,6 +215,14 @@ } } + &-pdf-2-line { + &:before, + &:after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M5 4H15V8H19V20H5V4ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM10.4999 7.5C10.4999 9.07749 10.0442 10.9373 9.27493 12.6534C8.50287 14.3757 7.46143 15.8502 6.37524 16.7191L7.55464 18.3321C10.4821 16.3804 13.7233 15.0421 16.8585 15.49L17.3162 13.5513C14.6435 12.6604 12.4999 9.98994 12.4999 7.5H10.4999ZM11.0999 13.4716C11.3673 12.8752 11.6042 12.2563 11.8037 11.6285C12.2753 12.3531 12.8553 13.0182 13.5101 13.5953C12.5283 13.7711 11.5665 14.0596 10.6352 14.4276C10.7999 14.1143 10.9551 13.7948 11.0999 13.4716Z'%3E%3C/path%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M5 4H15V8H19V20H5V4ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM10.4999 7.5C10.4999 9.07749 10.0442 10.9373 9.27493 12.6534C8.50287 14.3757 7.46143 15.8502 6.37524 16.7191L7.55464 18.3321C10.4821 16.3804 13.7233 15.0421 16.8585 15.49L17.3162 13.5513C14.6435 12.6604 12.4999 9.98994 12.4999 7.5H10.4999ZM11.0999 13.4716C11.3673 12.8752 11.6042 12.2563 11.8037 11.6285C12.2753 12.3531 12.8553 13.0182 13.5101 13.5953C12.5283 13.7711 11.5665 14.0596 10.6352 14.4276C10.7999 14.1143 10.9551 13.7948 11.0999 13.4716Z'%3E%3C/path%3E%3C/svg%3E"); + } + } + &-underline { &:before, &:after { From 62b64f5d2d6dcb7da8d970ebbb63ea5ef6744d6f Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 15 Jul 2024 23:29:49 +0200 Subject: [PATCH 15/16] conquer the world --- config/locales/models/export_templates/en.yml | 10 +++++----- config/locales/models/export_templates/fr.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/locales/models/export_templates/en.yml b/config/locales/models/export_templates/en.yml index 9ec65402c..1952e0bc0 100644 --- a/config/locales/models/export_templates/en.yml +++ b/config/locales/models/export_templates/en.yml @@ -6,14 +6,14 @@ en: export_template: hints: name: "The name will be visible by you and the other instructors" - tiptap_default_dossier_directory: "How would you like to name the directory containing the documents of a folder?" - tiptap_pdf_name: "How would you like to name the pdf file containing all the user's answers?" + dossier_folder: "How would you like to name the directory containing the documents of a folder?" + export_pdf: "How would you like to name the pdf file containing all the user's answers?" name: "Template's name" - tiptap_default_dossier_directory: "Directory's name for pdf format" - tiptap_pdf_name: "Export's filename" + dossier_folder: "Directory's name for pdf format" + export_pdf: "File in pdf format" errors: models: export_template: - dossier_number_mandatory: "must contain dossier's number" + dossier_number_required: "must contain dossier's number" different_templates: "Files must have different names" invalid_template: "A file name is invalid" diff --git a/config/locales/models/export_templates/fr.yml b/config/locales/models/export_templates/fr.yml index 04ef38215..9d152bbc5 100644 --- a/config/locales/models/export_templates/fr.yml +++ b/config/locales/models/export_templates/fr.yml @@ -6,14 +6,14 @@ fr: export_template: hints: name: "Le nom sera visible par vous et les autres instructeurs pour générer un export" - tiptap_default_dossier_directory: "Comment souhaitez-vous nommer le répertoire contenant les documents d'un dossier ?" - tiptap_pdf_name: "Comment souhaitez-vous nommer le fichier pdf qui contient toutes les réponses de l'usager ?" + dossier_folder: "Comment souhaitez-vous nommer le répertoire contenant les documents d'un dossier ?" + export_pdf: "Comment souhaitez-vous nommer le fichier pdf qui contient toutes les réponses de l'usager ?" name: "Nom du modèle" - tiptap_default_dossier_directory: Nom du répertoire - tiptap_pdf_name: "Nom du dossier au format pdf" + dossier_folder: Nom du répertoire + export_pdf: "Dossier au format pdf" errors: models: export_template: - dossier_number_mandatory: doit contenir le numéro du dossier + dossier_number_required: doit contenir le numéro du dossier different_templates: Les fichiers doivent avoir des noms différents invalid_template: Un nom de fichier est invalide From 0dbeb822e480cbdf0136443a0f2732794950cabe Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 26 Jul 2024 10:10:38 +0200 Subject: [PATCH 16/16] layout: add possibiilty to export outdated pjs --- .../export_templates/_form.html.haml | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/app/views/instructeurs/export_templates/_form.html.haml b/app/views/instructeurs/export_templates/_form.html.haml index 1de02b429..3f022902b 100644 --- a/app/views/instructeurs/export_templates/_form.html.haml +++ b/app/views/instructeurs/export_templates/_form.html.haml @@ -45,13 +45,33 @@ libelle: ExportTemplate.human_attribute_name(:export_pdf), prefix: 'export_template[export_pdf]' } - %h3 Pièces justificatives - - procedure.exportables_pieces_jointes.each do |tdc| - - item = export_template.pj(tdc) - = render partial: 'export_item', - locals: { item:, - libelle: tdc.libelle, - prefix: 'export_template[pjs][]'} + - if procedure.exportables_pieces_jointes_for_all_versions.any? + %h3 Pièces justificatives + + - procedure.exportables_pieces_jointes.each do |tdc| + - item = export_template.pj(tdc) + = render partial: 'export_item', + locals: { item:, + libelle: tdc.libelle, + prefix: 'export_template[pjs][]'} + + - outdated_tdcs = procedure.outdated_exportables_pieces_jointes + - outdated_stable_ids = outdated_tdcs.map(&:stable_id) + - expanded = export_template.pjs.filter(&:enabled?).any? { _1.stable_id.in?(outdated_stable_ids) } + + - if outdated_tdcs.any? + %section.fr-accordion.fr-mb-3w + %h3.fr-accordion__title + %button.fr-accordion__btn{ "aria-controls" => "accordion-106", "aria-expanded" => expanded.to_s, "type" => "button" } + pièces justificatives uniquement présentes dans les versions précédentes + .fr-collapse#accordion-106 + + - outdated_tdcs.each do |tdc| + - item = export_template.pj(tdc) + = render partial: 'export_item', + locals: { item:, + libelle: tdc.libelle, + prefix: 'export_template[pjs][]'} .fixed-footer .fr-container