Merge pull request #10713 from demarches-simplifiees/etq-admin-bug-message-info-character-limit

Tech : amélioration de la gestion des types_de_champ.options
This commit is contained in:
Benoit Queyron 2024-10-15 13:02:33 +00:00 committed by GitHub
commit c8f69283cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 230 additions and 42 deletions

View file

@ -67,7 +67,7 @@
.flex.justify-start.fr-mt-1w .flex.justify-start.fr-mt-1w.flex-gap
- if type_de_champ.drop_down_list? - if type_de_champ.drop_down_list?
.flex.column.justify-start.width-33 .flex.column.justify-start.width-33
.cell .cell

View file

@ -326,6 +326,7 @@ class Procedure < ApplicationRecord
Procedure.transaction do Procedure.transaction do
if brouillon? if brouillon?
reset! reset!
cleanup_types_de_champ_options!
end end
other_procedure = other_procedure_with_path(path) other_procedure = other_procedure_with_path(path)
@ -348,6 +349,12 @@ class Procedure < ApplicationRecord
end end
end end
def cleanup_types_de_champ_options!
draft_revision.types_de_champ.each do |type_de_champ|
type_de_champ.update!(options: type_de_champ.clean_options)
end
end
def suggested_path(administrateur) def suggested_path(administrateur)
if path_customized? if path_customized?
return path return path
@ -800,6 +807,7 @@ class Procedure < ApplicationRecord
def publish_revision! def publish_revision!
reset! reset!
cleanup_types_de_champ_options!
transaction do transaction do
self.published_revision = draft_revision self.published_revision = draft_revision
self.draft_revision = create_new_revision self.draft_revision = create_new_revision

View file

@ -440,7 +440,7 @@ class ProcedureRevision < ApplicationRecord
to_type_de_champ.filename_for_attachement(:notice_explicative)) to_type_de_champ.filename_for_attachement(:notice_explicative))
end end
elsif to_type_de_champ.textarea? elsif to_type_de_champ.textarea?
if from_type_de_champ.character_limit != to_type_de_champ.character_limit if from_type_de_champ.character_limit.presence != to_type_de_champ.character_limit.presence
changes << ProcedureRevisionChange::UpdateChamp.new(from_type_de_champ, changes << ProcedureRevisionChange::UpdateChamp.new(from_type_de_champ,
:character_limit, :character_limit,
from_type_de_champ.character_limit, from_type_de_champ.character_limit,

View file

@ -210,14 +210,10 @@ class TypeDeChamp < ApplicationRecord
before_validation :check_mandatory before_validation :check_mandatory
before_validation :normalize_libelle before_validation :normalize_libelle
before_save :remove_piece_justificative_template, if: -> { type_champ_changed? } before_save :remove_attachment, if: -> { type_champ_changed? }
before_validation :remove_drop_down_list, if: -> { type_champ_changed? } before_validation :set_drop_down_list_options, if: -> { type_champ_changed? }
before_save :remove_block, if: -> { type_champ_changed? } before_save :remove_block, if: -> { type_champ_changed? }
after_save if: -> { @remove_piece_justificative_template } do
piece_justificative_template.purge_later
end
def valid?(context = nil) def valid?(context = nil)
super super
if dynamic_type.present? if dynamic_type.present?
@ -680,6 +676,24 @@ class TypeDeChamp < ApplicationRecord
.parameterize .parameterize
end end
OPTS_BY_TYPE = {
type_champs.fetch(:header_section) => [:header_section_level],
type_champs.fetch(:explication) => [:collapsible_explanation_enabled, :collapsible_explanation_text],
type_champs.fetch(:textarea) => [:character_limit],
type_champs.fetch(:carte) => TypesDeChamp::CarteTypeDeChamp::LAYERS,
type_champs.fetch(:drop_down_list) => [:drop_down_other, :drop_down_options],
type_champs.fetch(:multiple_drop_down_list) => [:drop_down_options],
type_champs.fetch(:linked_drop_down_list) => [:drop_down_options, :drop_down_secondary_libelle, :drop_down_secondary_description],
type_champs.fetch(:piece_justificative) => [:old_pj, :skip_pj_validation, :skip_content_type_pj_validation],
type_champs.fetch(:titre_identite) => [:old_pj, :skip_pj_validation, :skip_content_type_pj_validation],
type_champs.fetch(:expression_reguliere) => [:expression_reguliere, :expression_reguliere_error_message, :expression_reguliere_exemple_text]
}
def clean_options
kept_keys = OPTS_BY_TYPE.fetch(type_champ.to_s) { [] }
options.slice(*kept_keys.map(&:to_s))
end
class << self class << self
def champ_value(type_champ, champ) def champ_value(type_champ, champ)
dynamic_type_class = type_champ_to_class_name(type_champ).constantize dynamic_type_class = type_champ_to_class_name(type_champ).constantize
@ -766,21 +780,19 @@ class TypeDeChamp < ApplicationRecord
end end
end end
def remove_piece_justificative_template def remove_attachment
if !piece_justificative? && piece_justificative_template.attached? if !piece_justificative? && piece_justificative_template.attached?
@remove_piece_justificative_template = true piece_justificative_template.purge_later
elsif !explication? && notice_explicative.attached?
notice_explicative.purge_later
end end
end end
def remove_drop_down_list def set_drop_down_list_options
if !drop_down_list? if (simple_drop_down_list? || multiple_drop_down_list?) && drop_down_options.empty?
self.drop_down_options = nil self.drop_down_options = ['Fromage', 'Dessert']
elsif !drop_down_options_changed? elsif linked_drop_down_list? && drop_down_options.none?(/^--.*--$/)
self.drop_down_options = if linked_drop_down_list? self.drop_down_options = ['--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes']
['--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes']
else
['Premier choix', 'Deuxième choix']
end
end end
end end

View file

@ -15,14 +15,15 @@ describe Procedure::Card::AnnotationsComponent, type: :component do
end end
context 'when errors on types_de_champs_public' do context 'when errors on types_de_champs_public' do
let(:types_de_champ_public) { [{ type: :drop_down_list, options: [] }] } let(:types_de_champ_public) { [{ type: :repetition, children: [] }] }
it 'does not render' do it 'does not render' do
expect(subject).to have_selector('.fr-badge--info', text: 'À configurer') expect(subject).to have_selector('.fr-badge--info', text: 'À configurer')
end end
end end
context 'when errors on types_de_champs_private' do context 'when errors on types_de_champs_private' do
let(:types_de_champ_private) { [{ type: :drop_down_list, options: [] }] } let(:types_de_champ_private) { [{ type: :repetition, children: [] }] }
it 'render the template' do it 'render the template' do
expect(subject).to have_selector('.fr-badge--error', text: 'À modifier') expect(subject).to have_selector('.fr-badge--error', text: 'À modifier')

View file

@ -15,14 +15,14 @@ describe Procedure::Card::ChampsComponent, type: :component do
end end
context 'when errors on types_de_champs_public' do context 'when errors on types_de_champs_public' do
let(:types_de_champ_public) { [{ type: :drop_down_list, options: [] }] } let(:types_de_champ_public) { [{ type: :repetition, children: [] }] }
it 'does not render' do it 'does not render' do
expect(subject).to have_selector('.fr-badge--error', text: 'À modifier') expect(subject).to have_selector('.fr-badge--error', text: 'À modifier')
end end
end end
context 'when errors on types_de_champs_private' do context 'when errors on types_de_champs_private' do
let(:types_de_champ_private) { [{ type: :drop_down_list, options: [] }] } let(:types_de_champ_private) { [{ type: :repetition, children: [] }] }
it 'render the template' do it 'render the template' do
expect(subject).to have_selector('.fr-badge--warning', text: 'À faire') expect(subject).to have_selector('.fr-badge--warning', text: 'À faire')

View file

@ -5,8 +5,8 @@ describe Procedure::ErrorsSummary, type: :component do
describe 'validations context' do describe 'validations context' do
let(:procedure) { create(:procedure, types_de_champ_private:, types_de_champ_public:) } let(:procedure) { create(:procedure, types_de_champ_private:, types_de_champ_public:) }
let(:types_de_champ_private) { [{ type: :drop_down_list, options: [], libelle: 'private' }] } let(:types_de_champ_private) { [{ type: :repetition, children: [], libelle: 'private' }] }
let(:types_de_champ_public) { [{ type: :drop_down_list, options: [], libelle: 'public' }] } let(:types_de_champ_public) { [{ type: :repetition, children: [], libelle: 'public' }] }
before { subject } before { subject }
@ -17,7 +17,7 @@ describe Procedure::ErrorsSummary, type: :component do
expect(page).to have_content("Erreur : Des problèmes empêchent la publication de la démarche") expect(page).to have_content("Erreur : Des problèmes empêchent la publication de la démarche")
expect(page).to have_selector("a", text: "public") expect(page).to have_selector("a", text: "public")
expect(page).to have_selector("a", text: "private") expect(page).to have_selector("a", text: "private")
expect(page).to have_text("doit comporter au moins un choix sélectionnable", count: 2) expect(page).to have_text("doit comporter au moins un champ répétable", count: 2)
end end
end end
@ -27,7 +27,7 @@ describe Procedure::ErrorsSummary, type: :component do
it 'shows errors and links for public only tdc' do it 'shows errors and links for public only tdc' do
expect(page).to have_text("Erreur : Les champs formulaire contiennent des erreurs") expect(page).to have_text("Erreur : Les champs formulaire contiennent des erreurs")
expect(page).to have_selector("a", text: "public") expect(page).to have_selector("a", text: "public")
expect(page).to have_text("doit comporter au moins un choix sélectionnable", count: 1) expect(page).to have_text("doit comporter au moins un champ répétable", count: 1)
expect(page).not_to have_selector("a", text: "private") expect(page).not_to have_selector("a", text: "private")
end end
end end
@ -38,7 +38,7 @@ describe Procedure::ErrorsSummary, type: :component do
it 'shows errors and links for private only tdc' do it 'shows errors and links for private only tdc' do
expect(page).to have_text("Erreur : Les annotations privées contiennent des erreurs") expect(page).to have_text("Erreur : Les annotations privées contiennent des erreurs")
expect(page).to have_selector("a", text: "private") expect(page).to have_selector("a", text: "private")
expect(page).to have_text("doit comporter au moins un choix sélectionnable") expect(page).to have_text("doit comporter au moins un champ répétable")
expect(page).not_to have_selector("a", text: "public") expect(page).not_to have_selector("a", text: "public")
end end
end end
@ -59,7 +59,11 @@ describe Procedure::ErrorsSummary, type: :component do
let(:validation_context) { :types_de_champ_public_editor } let(:validation_context) { :types_de_champ_public_editor }
before { subject } before do
drop_down_public = procedure.draft_revision.types_de_champ_public.find(&:drop_down_list?)
drop_down_public.update!(drop_down_options: [])
subject
end
it 'renders all errors and links on champ' do it 'renders all errors and links on champ' do
expect(page).to have_selector("a", text: "drop down list requires options") expect(page).to have_selector("a", text: "drop down list requires options")

View file

@ -3,26 +3,28 @@
describe TypesDeChampEditor::EditorComponent, type: :component do describe TypesDeChampEditor::EditorComponent, type: :component do
let(:revision) { procedure.draft_revision } let(:revision) { procedure.draft_revision }
let(:procedure) { create(:procedure, id: 1, types_de_champ_private:, types_de_champ_public:) } let(:procedure) { create(:procedure, id: 1, types_de_champ_private:, types_de_champ_public:) }
let(:types_de_champ_private) { [{ type: :repetition, children: [], libelle: 'private' }] }
let(:types_de_champ_private) { [{ type: :drop_down_list, options: [], libelle: 'private' }] } let(:types_de_champ_public) { [{ type: :repetition, children: [], libelle: 'public' }] }
let(:types_de_champ_public) { [{ type: :drop_down_list, options: [], libelle: 'public' }] }
describe 'render' do describe 'render' do
subject { render_inline(described_class.new(revision:, is_annotation:)) } subject { render_inline(described_class.new(revision:, is_annotation:)) }
context 'types_de_champ_public' do context 'types_de_champ_public' do
let(:is_annotation) { false } let(:is_annotation) { false }
it 'does not render private champs errors' do it 'does not render private champs errors' do
expect(subject).not_to have_text("private") expect(subject).not_to have_text("private")
expect(subject).to have_selector("a", text: "public") expect(subject).to have_selector("a", text: "public")
expect(subject).to have_text("doit comporter au moins un choix sélectionnable") expect(subject).to have_text("doit comporter au moins un champ répétable")
end end
end end
context 'types_de_champ_private' do context 'types_de_champ_private' do
let(:is_annotation) { true } let(:is_annotation) { true }
it 'does not render public champs errors' do it 'does not render public champs errors' do
expect(subject).to have_selector("a", text: "private") expect(subject).to have_selector("a", text: "private")
expect(subject).to have_text("doit comporter au moins un choix sélectionnable") expect(subject).to have_text("doit comporter au moins un champ répétable")
expect(subject).not_to have_text("public") expect(subject).not_to have_text("public")
end end
end end

View file

@ -537,6 +537,29 @@ describe ProcedureRevision do
end end
end end
context 'when a type de champ is transformed into a text_area with no character limit' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :text }]) }
before do
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
updated_tdc.update(type_champ: :textarea, options: { "character_limit" => "" })
end
it do
is_expected.to eq([
{
op: :update,
attribute: :type_champ,
label: first_tdc.libelle,
private: false,
from: "text",
to: "textarea",
stable_id: first_tdc.stable_id
}
])
end
end
context 'when a type de champ is moved' do context 'when a type de champ is moved' do
let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) } let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) }
let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second } let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second }

View file

@ -393,10 +393,12 @@ describe Procedure do
end end
it 'validates that no drop-down type de champ is empty' do it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication) drop_down = procedure.draft_revision.types_de_champ_public.find(&:drop_down_list?)
drop_down.update!(drop_down_options: [])
procedure.reload.validate(:publication)
expect(procedure.errors.messages_for(:draft_types_de_champ_public)).to include(invalid_drop_down_error_message) expect(procedure.errors.messages_for(:draft_types_de_champ_public)).to include(invalid_drop_down_error_message)
drop_down = procedure.draft_revision.types_de_champ_public.find(&:drop_down_list?)
drop_down.update!(drop_down_options: ["--title--", "some value"]) drop_down.update!(drop_down_options: ["--title--", "some value"])
procedure.reload.validate(:publication) procedure.reload.validate(:publication)
expect(procedure.errors.messages_for(:draft_types_de_champ_public)).not_to include(invalid_drop_down_error_message) expect(procedure.errors.messages_for(:draft_types_de_champ_public)).not_to include(invalid_drop_down_error_message)
@ -418,14 +420,17 @@ describe Procedure do
it 'validates that no repetition type de champ is empty' do it 'validates that no repetition type de champ is empty' do
procedure.validate(:publication) procedure.validate(:publication)
expect(procedure.errors.messages_for(:draft_types_de_champ_private)).to include(invalid_repetition_error_message) expect(procedure.errors.messages_for(:draft_types_de_champ_private)).to include(invalid_repetition_error_message)
repetition = procedure.draft_revision.types_de_champ_private.find(&:repetition?) repetition = procedure.draft_revision.types_de_champ_private.find(&:repetition?)
expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(repetition) expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(repetition)
end end
it 'validates that no drop-down type de champ is empty' do it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication)
expect(procedure.errors.messages_for(:draft_types_de_champ_private)).to include(invalid_drop_down_error_message)
drop_down = procedure.draft_revision.types_de_champ_private.find(&:drop_down_list?) drop_down = procedure.draft_revision.types_de_champ_private.find(&:drop_down_list?)
drop_down.update!(drop_down_options: [])
procedure.reload.validate(:publication)
expect(procedure.errors.messages_for(:draft_types_de_champ_private)).to include(invalid_drop_down_error_message)
expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(drop_down) expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(drop_down)
end end
end end

View file

@ -107,19 +107,22 @@ describe TypeDeChamp do
context 'when the target type_champ is not drop_down_list' do context 'when the target type_champ is not drop_down_list' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) }
it { expect(tdc.drop_down_options).to be_empty } it { expect(tdc.drop_down_options).to be_present }
it { expect(tdc.drop_down_options).to eq(["val1", "val2", "val3"]) }
end end
context 'when the target type_champ is linked_drop_down_list' do context 'when the target type_champ is linked_drop_down_list' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) } let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) }
it { expect(tdc.drop_down_options).to be_present } it { expect(tdc.drop_down_options).to be_present }
it { expect(tdc.drop_down_options).to eq(['--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes']) }
end end
context 'when the target type_champ is multiple_drop_down_list' do context 'when the target type_champ is multiple_drop_down_list' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) } let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) }
it { expect(tdc.drop_down_options).to be_present } it { expect(tdc.drop_down_options).to be_present }
it { expect(tdc.drop_down_options).to eq(["val1", "val2", "val3"]) }
end end
end end
@ -287,4 +290,127 @@ describe TypeDeChamp do
it { is_expected.to eq("1-tres-interessant-bilan") } it { is_expected.to eq("1-tres-interessant-bilan") }
end end
describe '#clean_options' do
subject { procedure.published_revision.types_de_champ.first.options }
let(:procedure) { create(:procedure) }
context "Header section" do
let(:type_de_champ) { create(:type_de_champ_header_section, procedure:) }
before do
type_de_champ.update!(options: { 'header_section_level' => '1', 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the header_section_level' do
is_expected.to eq({ 'header_section_level' => '1' })
end
end
context "Explication" do
let(:type_de_champ) { create(:type_de_champ_explication, procedure:) }
before do
type_de_champ.update!(options: { 'collapsible_explanation_enabled' => '1', 'collapsible_explanation_text' => 'hello', 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the collapsible_explanation keys' do
is_expected.to eq({ 'collapsible_explanation_enabled' => '1', 'collapsible_explanation_text' => 'hello' })
end
end
context "Text area" do
let(:type_de_champ) { create(:type_de_champ_textarea, procedure:) }
before do
type_de_champ.update!(options: { 'character_limit' => '400', 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the character limit' do
is_expected.to eq({ 'character_limit' => '400' })
end
end
context "Carte" do
let(:type_de_champ) { create(:type_de_champ_carte, procedure:) }
before do
type_de_champ.update!(options: { 'unesco' => '0', 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the layers' do
is_expected.to eq({ 'unesco' => '0' })
end
end
context "Simple drop down_list" do
let(:type_de_champ) { create(:type_de_champ_drop_down_list, procedure:) }
before do
type_de_champ.update!(options: { 'drop_down_other' => '0', 'drop_down_options' => ['Premier choix', 'Deuxième choix'], 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the drop_down_other and drop_down_options' do
is_expected.to eq({ 'drop_down_other' => '0', 'drop_down_options' => ['Premier choix', 'Deuxième choix'] })
end
end
context "Multiple drop down_list" do
let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, procedure:) }
before do
type_de_champ.update!(options: { 'drop_down_options' => ['Premier choix', 'Deuxième choix'], 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the drop_down_options' do
is_expected.to eq({ 'drop_down_options' => ['Premier choix', 'Deuxième choix'] })
end
end
context "Linked drop down list" do
let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list, procedure:) }
before do
type_de_champ.update!(options: { 'drop_down_options' => ['--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes'], 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the drop_down_options' do
is_expected.to eq({ 'drop_down_options' => ['--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes'] })
end
end
context "Piece justificative" do
let(:type_de_champ) { create(:type_de_champ_piece_justificative, procedure:) }
before do
type_de_champ.update!(options: { 'old_pj' => '123', 'skip_pj_validation' => '1', 'skip_content_type_pj_validation' => '1', 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the old_pj, skip_validation_pj and skip_content_type_pj_validation' do
is_expected.to eq({ 'old_pj' => '123', 'skip_pj_validation' => '1', 'skip_content_type_pj_validation' => '1' })
end
end
context "Expression reguliere" do
let(:type_de_champ) { create(:type_de_champ_expression_reguliere, procedure:) }
before do
type_de_champ.update!(options: { 'expression_reguliere' => '\d{9}', 'expression_reguliere_error_message' => 'error', 'expression_reguliere_exemple_text' => '123456789', 'key' => 'value' })
procedure.publish_revision!
end
it 'keeping only the expression_reguliere, expression_reguliere_error_message and expression_reguliere_exemple_text' do
is_expected.to eq({ 'expression_reguliere' => '\d{9}', 'expression_reguliere_error_message' => 'error', 'expression_reguliere_exemple_text' => '123456789' })
end
end
end
end end

View file

@ -16,7 +16,7 @@ RSpec.describe TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp do
context 'when the multiple drop down list has no option' do context 'when the multiple drop down list has no option' do
let(:drop_down_options_from_text) { "" } let(:drop_down_options_from_text) { "" }
it { expect(example_value).to eq(nil) } it { expect(example_value).to eq(["Fromage", "Dessert"]) }
end end
context 'when the multiple drop down list only has one option' do context 'when the multiple drop down list only has one option' do

View file

@ -66,8 +66,15 @@ describe 'Publishing a procedure', js: true do
:with_zone, :with_zone,
instructeurs: instructeurs, instructeurs: instructeurs,
administrateur: administrateur, administrateur: administrateur,
types_de_champ_public: [{ type: :repetition, libelle: 'Enfants', children: [] }, { type: :drop_down_list, libelle: 'Civilité', options: [] }], types_de_champ_public: [{ type: :repetition, libelle: 'Enfants', children: [] }, { type: :drop_down_list, libelle: 'Civilité' }],
types_de_champ_private: [{ type: :drop_down_list, libelle: 'Civilité', options: [] }]) types_de_champ_private: [{ type: :drop_down_list, libelle: 'Civilité' }])
end
before do
drop_down = procedure.draft_revision.types_de_champ_public.find(&:drop_down_list?)
drop_down.update!(drop_down_options: [])
drop_down = procedure.draft_revision.types_de_champ_private.find(&:drop_down_list?)
drop_down.update!(drop_down_options: [])
end end
scenario 'an error message prevents the publication' do scenario 'an error message prevents the publication' do