feat(attestation): list tags errors and substitute missing tag by libelle

This commit is contained in:
Colin Darie 2024-02-01 18:55:41 +01:00
parent 6f49dd892d
commit f7484eb0e5
No known key found for this signature in database
GPG key ID: 8C76CADD40253590
16 changed files with 139 additions and 55 deletions

View file

@ -52,6 +52,8 @@ module Administrateurs
['Redo', 'redo', 'arrow-go-forward-line']
]
]
@attestation_template.validate
end
def update
@ -67,15 +69,11 @@ module Administrateurs
attestation_params[:signature] = uninterlace_png(signature_file)
end
@attestation_template.update!(attestation_params)
flash.notice = "Le modèle de lattestation a été modifié"
respond_to do |format|
format.turbo_stream
format.html do
redirect_to edit_admin_procedure_attestation_template_path(@procedure)
end
if !@attestation_template.update(attestation_params)
flash.alert = "Le modèle de lattestation contient des erreurs et n'a pas pu être enregistré. Corriger les erreurs."
end
render :update
end
def create = update

View file

@ -251,8 +251,15 @@ module TagsSubstitutionConcern
}.reject { |_, ary| ary.empty? }
end
def used_type_de_champ_tags(text)
used_tags_and_libelle_for(text).filter_map do |(tag, libelle)|
def used_type_de_champ_tags(text_or_tiptap)
used_tags =
if text_or_tiptap.respond_to?(:deconstruct_keys) # hash pattern matching
TiptapService.new.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
else
used_tags_and_libelle_for(text_or_tiptap.to_s)
end
used_tags.filter_map do |(tag, libelle)|
if tag.nil?
[libelle]
elsif !tag.in?(SHARED_TAG_IDS) && tag.start_with?('tdc')
@ -265,11 +272,11 @@ module TagsSubstitutionConcern
used_tags_and_libelle_for(text).map { _1.first.nil? ? _1.second : _1.first }
end
def tags_substitutions(tokens, dossier, escape: true)
def tags_substitutions(tags_and_libelles, dossier, escape: true)
# NOTE:
# - tokens est un simple Set d'ids (pas la même structure que dans replace_tags)
# - dans replace_tags, on fait référence à des tags avec ou sans id, mais pas ici,
# a priori inutile car tiptap ne fait référence qu'aux ids.
# - tags_and_libelles est un simple Set de couples (tag_id, libelle) (pas la même structure que dans replace_tags)
# - dans `replace_tags`, on fait référence à des tags avec ou sans id, mais pas ici,
# (inutile car tiptap ne référence que des ids)
@escape_unsafe_tags = escape
@ -283,12 +290,12 @@ module TagsSubstitutionConcern
end
end
tokens.index_with do |token|
case flat_tags[token]
tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions|
substitutions[tag_id] = case flat_tags[tag_id]
in tag, data
replace_tag(tag, data)
else
token
else # champ not in dossier, for example during preview on draft revision
libelle
end
end
end

View file

@ -5,13 +5,14 @@ class TiptapService
children(node[:content], substitutions, 0)
end
def used_tags(node, tags = Set.new)
# NOTE: node must be deep symbolized keys
def used_tags_and_libelle_for(node, tags = Set.new)
case node
in type: 'mention', attrs: { id: }
tags << id
in { content: } if content.is_a?(Array)
content.each { used_tags(_1, tags) }
else
in type: 'mention', attrs: { id:, label: }, **rest
tags << [id, label]
in { content:, **rest } if content.is_a?(Array)
content.each { used_tags_and_libelle_for(_1, tags) }
in type:, **rest
# noop
end

View file

@ -1,16 +1,16 @@
class TagsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
procedure = record.procedure
tags = record.used_type_de_champ_tags(value.to_s)
tags = record.used_type_de_champ_tags(value)
invalid_tags = tags.filter_map do |(tag, stable_id)|
tag if stable_id.nil?
end
invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision)
invalid_for_draft_revision = invalid_tags_for_revision(record, tags, procedure.draft_revision)
invalid_for_published_revision = if procedure.published_revision_id.present?
invalid_tags_for_revision(record, attribute, tags, procedure.published_revision)
invalid_tags_for_revision(record, tags, procedure.published_revision)
else
[]
end
@ -18,7 +18,7 @@ class TagsValidator < ActiveModel::EachValidator
invalid_for_previous_revision = procedure
.revisions_with_pending_dossiers
.flat_map do |revision|
invalid_tags_for_revision(record, attribute, tags, revision)
invalid_tags_for_revision(record, tags, revision)
end.uniq
# champ is added in draft revision but not yet published
@ -48,7 +48,7 @@ class TagsValidator < ActiveModel::EachValidator
end
end
def invalid_tags_for_revision(record, attribute, tags, revision)
def invalid_tags_for_revision(record, tags, revision)
revision_stable_ids = revision
.revision_types_de_champ
.filter { !_1.child? }

View file

@ -1 +1,2 @@
#autosave-notice.fr-badge.fr-badge--sm.fr-badge--success= t(".form_saved")
- success = local_assigns.fetch(:success, true)
#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success, "fr-badge--error" => !success) }= success ? t(".form_saved") : t(".form_error")

View file

@ -7,7 +7,7 @@
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), html: { multipart: true },
data: { turbo: 'true',
controller: 'autosubmit attestation',
autosubmit_debounce_delay_value: 2000,
autosubmit_debounce_delay_value: 1000,
attestation_logo_attachment_official_label_value: AttestationTemplate.human_attribute_name(:logo_additional),
attestation_logo_attachment_free_label_value: AttestationTemplate.human_attribute_name(:logo) } do |f|
@ -65,12 +65,17 @@
- c.with_hint { "Exemple: Direction interministérielle du numérique. 2 lignes maximum" }
.fr-fieldset__element.fr-mt-2w
%label.fr-label.fr-h4
= AttestationTemplate.human_attribute_name :body
= render EditableChamp::AsteriskMandatoryComponent.new
.fr-input-group{ class: class_names("fr-input-group--error" => f.object.errors.include?(:json_body)) }
%label.fr-label.fr-h4
= AttestationTemplate.human_attribute_name :body
= render EditableChamp::AsteriskMandatoryComponent.new
.editor{ data: { tiptap_target: 'editor' } }
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
#editor.editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} }
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
.fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
- if f.object.errors.include?(:json_body)
= render partial: "shared/errors_list", locals: { object: f.object, attribute: :json_body }
.fr-fieldset__element
.flex.flex-gap-2

View file

@ -1,5 +1,5 @@
= turbo_stream.show 'autosave-notice'
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice')
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice', locals: { success: !@attestation_template.changed? })
= turbo_stream.hide 'autosave-notice', delay: 15000
- if @attestation_template.logo_blob&.previously_new_record?
@ -9,3 +9,12 @@
- if @attestation_template.signature_blob&.previously_new_record?
= turbo_stream.update dom_id(@attestation_template, :signature_attachment) do
= render(Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false))
- body_id = dom_id(@attestation_template, "json-body-messages")
- if @attestation_template.errors.include?(:json_body)
= turbo_stream.update body_id do
= render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body }
= turbo_stream.show body_id
- else
= turbo_stream.hide body_id
= turbo_stream.update body_id, nil

View file

@ -0,0 +1,3 @@
%ul.list-style-type-none.fr-pl-0
- object.errors.full_messages_for(attribute).map do |error_message|
%li= error_message

View file

@ -20,8 +20,8 @@ fr:
one: contient la balise "%{tags}" qui nexiste pas. Supprimer la balise
other: contient %{count} balises (%{tags}) qui nexistent pas. Supprimer les balises
champ_missing_in_draft_revision:
one: contient la balise "%{tags}" qui a été supprimée mais la suppression nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer
other: contient %{count} balises (%{tags}) qui ont été supprimées mais la suppression nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer
one: contient la balise "%{tags}" qui a été supprimée dans les modifications en cours du formulaire. Supprimer cette balise ou réinitialiser les modifications du formulaire puis recommencer
other: contient %{count} balises (%{tags}) qui ont été supprimées dans les modifications en coure du formumlaire. Supprimer cette balise ou réinitialiser les modifications du formulaire puis recommencer
champ_missing_in_published_revision:
one: contient la balise "%{tags}" qui nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer
other: contient %{count} balises (%{tags}) qui ne sont pas encore publiées. Publier la nouvelle version de la démarche et recommencer
@ -38,3 +38,6 @@ fr:
body:
format: Le champ « Contenu de lattestation » %{message}
<<: *tags_errors
json_body:
format: Le champ « Contenu de lattestation » %{message}
<<: *tags_errors

View file

@ -53,3 +53,4 @@ en:
submit: Publish
autosave_notice:
form_saved: "Form saved"
form_error: "Form in error"

View file

@ -53,3 +53,4 @@ fr:
submit: Publier
autosave_notice:
form_saved: "Formulaire enregistré"
form_error: "Formulaire en erreur"

View file

@ -14,12 +14,12 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
activated: false,
tiptap_body: {
type: :doc,
content: [
{
type: :paragraph,
content: [{ text: "Yo from spec" }]
}
]
content: [
{
type: :paragraph,
content: [{ text: "Yo from spec", type: :text }]
}
]
}.to_json
}
end
@ -131,11 +131,13 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
let(:attestation_template) { nil }
subject do
post :create, params: { procedure_id: procedure.id, attestation_template: update_params }
post :create, params: { procedure_id: procedure.id, attestation_template: update_params }, format: :turbo_stream
response.body
end
context 'when attestation template is valid' do
render_views
it "create template" do
subject
attestation_template = procedure.reload.attestation_template
@ -147,7 +149,7 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
expect(attestation_template.activated).to eq(false)
expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body])
expect(flash.notice).to be_present
expect(response.body).to include("Formulaire enregistré")
end
context "with files" do
@ -165,8 +167,9 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
end
describe 'PATCH update' do
render_views
subject do
patch :update, params: { procedure_id: procedure.id, attestation_template: update_params }
patch :update, params: { procedure_id: procedure.id, attestation_template: update_params }, format: :turbo_stream
response.body
end
@ -182,7 +185,7 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
expect(attestation_template.activated).to eq(false)
expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body])
expect(flash.notice).to be_present
expect(response.body).to include("Formulaire enregistré")
end
context "with files" do
@ -196,6 +199,18 @@ describe Administrateurs::AttestationTemplateV2sController, type: :controller do
expect(attestation_template.signature.download).to eq(signature.read)
end
end
context 'with error' do
let(:update_params) do
super().merge(tiptap_body: { type: :doc, content: [{ type: :mention, attrs: { id: "tdc12", label: "oops" } }] }.to_json)
end
it "render error" do
subject
expect(response.body).to include("Formulaire en erreur")
expect(response.body).to include('Supprimer cette balise')
end
end
end
end
end

View file

@ -241,7 +241,7 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
render_views
let(:body) { "body --#{removed_type_de_champ.libelle}--" }
it { expect(response.body).to have_content("Le champ « Contenu de lattestation » contient la balise \"#{removed_type_de_champ.libelle}\" qui a été supprimée mais la suppression nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer") }
it { expect(response.body).to have_content("Le champ « Contenu de lattestation » contient la balise \"#{removed_type_de_champ.libelle}\" qui a été supprimée dans les modifications en cours du formulaire. Supprimer cette balise ou réinitialiser les modifications du formulaire puis recommencer") }
end
context 'with removed and published' do

View file

@ -37,20 +37,29 @@ describe TagsSubstitutionConcern, type: :model do
let(:etablissement) { create(:etablissement) }
let(:dossier) { create(:dossier, :en_construction, procedure:, individual:, etablissement:) }
let(:instructeur) { create(:instructeur) }
let(:tags) { Set.new(["dossier_number"]) }
let(:tags) { Set.new([["dossier_number", "numéro de dossier"]]) }
subject { template_concern.tags_substitutions(tags, dossier) }
context 'dossiers metadata' do
before { travel_to(Time.zone.local(2024, 1, 15, 12)) }
let(:tags) { Set.new(["dossier_number", "dossier_depose_at", "dossier_processed_at", "dossier_procedure_libelle"]) }
let(:tags) do
Set.new([
["dossier_number", "n° de dossier"],
["dossier_depose_at", "date de dépôt"],
["dossier_processed_at", "date dinstruction"],
["dossier_procedure_libelle", "Nom de la démarche"],
["tdc_123", "Un champ"]
])
end
it do
is_expected.to eq(
"dossier_number" => dossier.id.to_s,
"dossier_depose_at" => "15/01/2024",
"dossier_processed_at" => "",
"dossier_procedure_libelle" => procedure.libelle
"dossier_procedure_libelle" => procedure.libelle,
"tdc_123" => "Un champ"
)
end
end

View file

@ -57,7 +57,7 @@ RSpec.describe TiptapService do
},
{
type: 'mention',
attrs: { id: 'name' },
attrs: { id: 'name', label: 'Nom' },
marks: [{ type: 'bold' }, { type: 'underline' }]
},
{
@ -173,7 +173,7 @@ RSpec.describe TiptapService do
describe '#used_tags' do
it 'returns used tags' do
expect(described_class.new.used_tags(json)).to eq(Set.new(['name']))
expect(described_class.new.used_tags_and_libelle_for(json)).to eq(Set.new([['name', 'Nom']]))
end
end
end

View file

@ -90,6 +90,7 @@ describe 'As an administrateur, I want to manage the procedures attestation',
}
expect(attestation.label_logo).to eq("System Test")
expect(attestation.activated?).to be_falsey
expect(page).to have_content("Formulaire enregistré")
click_on "date de décision"
@ -126,5 +127,35 @@ describe 'As an administrateur, I want to manage the procedures attestation',
fill_in "Contenu du pied de page", with: ["line1", "line2", "line3", "line4"].join("\n")
expect(page).to have_field("Contenu du pied de page", with: "line1\nline2\nline3line4")
end
context "tag in error" do
before do
tdc = procedure.active_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age')
procedure.publish_revision!
attestation = procedure.build_attestation_template_v2(json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT, label_logo: "test")
attestation.json_body["content"] << { type: :mention, attrs: { id: "tdc#{tdc.stable_id}", label: tdc.libelle } }
attestation.save!
procedure.draft_revision.remove_type_de_champ(tdc)
end
scenario do
visit edit_admin_procedure_attestation_template_v2_path(procedure)
expect(page).to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
click_on "date de décision"
expect(page).to have_content("Formulaire en erreur")
expect(page).to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
page.execute_script("document.getElementById('attestation_template_tiptap_body').type = 'text'")
fill_in "attestation_template[tiptap_body]", with: AttestationTemplate::TIPTAP_BODY_DEFAULT.to_json
expect(page).to have_content("Formulaire enregistré")
expect(page).not_to have_content("Formulaire en erreur")
expect(page).not_to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
end
end
end
end