From 979b5101ae80d2e9f4ca7cd69dc3d12ef0407297 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Fri, 13 Oct 2023 11:30:22 +0200 Subject: [PATCH 01/10] chore(conditions): champs_conditions_component and routing_rules_component inherit from conditions_component --- .../conditions/champs_conditions_component.rb | 44 +++++++++++++++++ .../champs_conditions_component.fr.yml} | 0 .../champs_conditions_component.html.haml} | 2 +- .../conditions_component.rb | 49 +++---------------- .../conditions/routing_rules_component.rb | 28 +++++++++++ .../routing_rules_component.fr.yml | 6 +++ .../routing_rules_component.html.haml | 25 ++++++++++ .../one_groupe_management_component.html.haml | 32 +++--------- .../champ_component/champ_component.html.haml | 2 +- .../champs_conditions_component_spec.rb} | 2 +- 10 files changed, 120 insertions(+), 70 deletions(-) create mode 100644 app/components/conditions/champs_conditions_component.rb rename app/components/{types_de_champ_editor/conditions_component/conditions_component.fr.yml => conditions/champs_conditions_component/champs_conditions_component.fr.yml} (100%) rename app/components/{types_de_champ_editor/conditions_component/conditions_component.html.haml => conditions/champs_conditions_component/champs_conditions_component.html.haml} (95%) rename app/components/{types_de_champ_editor => conditions}/conditions_component.rb (80%) create mode 100644 app/components/conditions/routing_rules_component.rb create mode 100644 app/components/conditions/routing_rules_component/routing_rules_component.fr.yml create mode 100644 app/components/conditions/routing_rules_component/routing_rules_component.html.haml rename spec/components/{types_de_champ_editor/conditions_component_spec.rb => conditions/champs_conditions_component_spec.rb} (99%) diff --git a/app/components/conditions/champs_conditions_component.rb b/app/components/conditions/champs_conditions_component.rb new file mode 100644 index 000000000..27f85275c --- /dev/null +++ b/app/components/conditions/champs_conditions_component.rb @@ -0,0 +1,44 @@ +class Conditions::ChampsConditionsComponent < Conditions::ConditionsComponent + def initialize(tdc:, upper_tdcs:, procedure_id:) + @tdc, @condition, @source_tdcs = tdc, tdc.condition, upper_tdcs + @procedure_id = procedure_id + end + + private + + def logic_conditionnel_button + html_class = 'fr-btn fr-btn--tertiary fr-btn--sm' + + if @condition.nil? + submit_tag( + t('.enable_conditionnel'), + formaction: add_condition_path, + class: html_class + ) + else + submit_tag( + t('.disable_conditionnel'), + formmethod: 'delete', + formnovalidate: true, + data: { confirm: t('.disable_conditionnel_alert') }, + class: html_class + ) + end + end + + def add_condition_path + add_row_admin_procedure_condition_path(@procedure_id, @tdc.stable_id) + end + + def delete_condition_path(row_index) + delete_row_admin_procedure_condition_path(@procedure_id, @tdc.stable_id, row_index: row_index) + end + + def input_id_for(name, row_index) + "#{@tdc.stable_id}-#{name}-#{row_index}" + end + + def input_prefix + 'type_de_champ[condition_form]' + end +end diff --git a/app/components/types_de_champ_editor/conditions_component/conditions_component.fr.yml b/app/components/conditions/champs_conditions_component/champs_conditions_component.fr.yml similarity index 100% rename from app/components/types_de_champ_editor/conditions_component/conditions_component.fr.yml rename to app/components/conditions/champs_conditions_component/champs_conditions_component.fr.yml diff --git a/app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml b/app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml similarity index 95% rename from app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml rename to app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml index 82e15064b..f9e45c340 100644 --- a/app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml +++ b/app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml @@ -6,7 +6,7 @@ Logique conditionnelle = logic_conditionnel_button - = render TypesDeChampEditor::ConditionsErrorsComponent.new(conditions: condition_per_row, upper_tdcs: @upper_tdcs) + = render TypesDeChampEditor::ConditionsErrorsComponent.new(conditions: condition_per_row, upper_tdcs: @source_tdcs) - if @condition.present? %table.condition-table.mt-2.width-100 diff --git a/app/components/types_de_champ_editor/conditions_component.rb b/app/components/conditions/conditions_component.rb similarity index 80% rename from app/components/types_de_champ_editor/conditions_component.rb rename to app/components/conditions/conditions_component.rb index d7013af9f..833e2c89d 100644 --- a/app/components/types_de_champ_editor/conditions_component.rb +++ b/app/components/conditions/conditions_component.rb @@ -1,11 +1,6 @@ -class TypesDeChampEditor::ConditionsComponent < ApplicationComponent +class Conditions::ConditionsComponent < ApplicationComponent include Logic - def initialize(tdc:, upper_tdcs:, procedure_id:) - @tdc, @condition, @upper_tdcs = tdc, tdc.condition, upper_tdcs - @procedure_id = procedure_id - end - private def rows @@ -20,26 +15,6 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent end end - def logic_conditionnel_button - html_class = 'fr-btn fr-btn--tertiary fr-btn--sm' - - if @condition.nil? - submit_tag( - t('.enable_conditionnel'), - formaction: add_row_admin_procedure_condition_path(@procedure_id, @tdc.stable_id), - class: html_class - ) - else - submit_tag( - t('.disable_conditionnel'), - formmethod: 'delete', - formnovalidate: true, - data: { confirm: t('.disable_conditionnel_alert') }, - class: html_class - ) - end - end - def far_left_tag(row_number) if row_number == 0 t('.display_if') @@ -85,7 +60,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent end def available_targets_for_select - @upper_tdcs + @source_tdcs .filter { |tdc| ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(tdc.type_champ) } .map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] } end @@ -108,7 +83,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent end def compatibles_operators_for_select(left) - case left.type(@upper_tdcs) + case left.type(@source_tdcs) when ChampValue::CHAMP_VALUE_TYPE.fetch(:boolean) [ [t('is', scope: 'logic'), Eq.name] @@ -138,7 +113,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent def right_operand_tag(left, right, row_index) right_invalid = !current_right_valid?(left, right) - case left.type(@upper_tdcs) + case left.type(@source_tdcs) when :boolean booleans_for_select = [[t('utils.yes'), constant(true).to_json], [t('utils.no'), constant(false).to_json]] @@ -160,7 +135,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent class: 'fr-select' ) when :enum, :enums - enums_for_select = left.options(@upper_tdcs) + enums_for_select = left.options(@source_tdcs) if right_invalid enums_for_select = empty_target_for_select + enums_for_select @@ -186,13 +161,13 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent end def current_right_valid?(left, right) - Logic.compatible_type?(left, right, @upper_tdcs) + Logic.compatible_type?(left, right, @source_tdcs) end def add_condition_tag tag.button( t('.add_condition'), - formaction: add_row_admin_procedure_condition_path(@procedure_id, @tdc.stable_id), + formaction: add_condition_path, formnovalidate: true, class: 'fr-btn fr-btn--secondary fr-btn--sm fr-icon-add-circle-line fr-btn--icon-left' ) @@ -201,7 +176,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent def delete_condition_tag(row_index) tag.button( tag.span('', class: 'icon delete') + tag.span(t('.remove_a_row'), class: 'sr-only'), - formaction: delete_row_admin_procedure_condition_path(@procedure_id, @tdc.stable_id, row_index: row_index), + formaction: delete_condition_path(row_index), formmethod: 'delete', formnovalidate: true ) @@ -214,12 +189,4 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent def input_name_for(name) "#{input_prefix}[rows][][#{name}]" end - - def input_id_for(name, row_index) - "#{@tdc.stable_id}-#{name}-#{row_index}" - end - - def input_prefix - 'type_de_champ[condition_form]' - end end diff --git a/app/components/conditions/routing_rules_component.rb b/app/components/conditions/routing_rules_component.rb new file mode 100644 index 000000000..4db0f0d19 --- /dev/null +++ b/app/components/conditions/routing_rules_component.rb @@ -0,0 +1,28 @@ +class Conditions::RoutingRulesComponent < Conditions::ConditionsComponent + include Logic + + def initialize(groupe_instructeur:) + @groupe_instructeur = groupe_instructeur + @condition = groupe_instructeur.routing_rule || empty_operator(empty, empty) + @procedure_id = groupe_instructeur.procedure_id + @source_tdcs = groupe_instructeur.procedure.active_revision.types_de_champ_public + end + + private + + def add_condition_path + add_row_admin_procedure_routing_rule_path(@procedure_id, @groupe_instructeur.id) + end + + def delete_condition_path(row_index) + delete_row_admin_procedure_routing_rule_path(@procedure_id, @groupe_instructeur.id, row_index: row_index) + end + + def input_id_for(name, row_index) + "#{@groupe_instructeur.id}-#{name}-#{row_index}" + end + + def input_prefix + 'groupe_instructeur[condition_form]' + end +end diff --git a/app/components/conditions/routing_rules_component/routing_rules_component.fr.yml b/app/components/conditions/routing_rules_component/routing_rules_component.fr.yml new file mode 100644 index 000000000..62932bb04 --- /dev/null +++ b/app/components/conditions/routing_rules_component/routing_rules_component.fr.yml @@ -0,0 +1,6 @@ +--- +fr: + display_if: Router si + select: Sélectionner + add_condition: Ajouter une règle de routage + remove_a_row: Supprimer la ligne diff --git a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml new file mode 100644 index 000000000..830e17526 --- /dev/null +++ b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml @@ -0,0 +1,25 @@ +.flex.justify-start.section{ id: dom_id(@groupe_instructeur, :routing_rule) } + = form_tag admin_procedure_routing_rule_path(@procedure_id, @groupe_instructeur.id), + method: :patch, + data: { turbo: true, controller: 'autosave' }, + class: 'form width-100' do + .conditionnel.mt-2.width-100 + + %table.condition-table.mt-2 + %thead + %tr + %th.far-left + %th.target Champ Cible + %th.operator Opérateur + %th.value Valeur + %th.delete-column + %tbody + - rows.each.with_index do |(targeted_champ, operator_name, value), row_index| + %tr + %td.far-left= far_left_tag(row_index) + %td.target= left_operand_tag(targeted_champ, row_index) + %td.operator= operator_tag(operator_name, targeted_champ, row_index) + %td.value= right_operand_tag(targeted_champ, value, row_index) + %td.delete-column= delete_condition_tag(row_index) + + .flex.justify-end.mt-2= add_condition_tag diff --git a/app/components/procedure/one_groupe_management_component/one_groupe_management_component.html.haml b/app/components/procedure/one_groupe_management_component/one_groupe_management_component.html.haml index 0aa12ca39..80d583cb9 100644 --- a/app/components/procedure/one_groupe_management_component/one_groupe_management_component.html.haml +++ b/app/components/procedure/one_groupe_management_component/one_groupe_management_component.html.haml @@ -22,34 +22,14 @@ Groupe inactif %span.fr-hint-text Si cette option est activée, les usagers ne pourront plus sélectionner ce groupe d’instructeurs - = form_tag admin_procedure_routing_rules_path(@procedure), - method: :post, - data: { controller: 'autosave' }, - class: 'fr-mb-3w' do + = render Conditions::RoutingRulesComponent.new(groupe_instructeur: @groupe_instructeur) - = hidden_field_tag('groupe_instructeur_id', @groupe_instructeur.id) + .fr-hint-text.mt-2.mb-2 + %span Si vous ne trouvez pas l'option correspondant à votre groupe, veuillez l'ajouter dans le + %span + = link_to 'champ concerné', champs_admin_procedure_path(@procedure) - .flex - %p.fr-mb-1w.fr-mr-2w Routage - - if @groupe_instructeur.invalid_rule? - %p.fr-mb-1w.fr-badge.fr-badge--warning.fr-badge--sm règle invalide - - elsif @groupe_instructeur.non_unique_rule? - %p.fr-mb-1w.fr-badge.fr-badge--warning.fr-badge--sm règle déjà attribuée à #{@groupe_instructeur.groups_with_same_rule} - - .flex.align-baseline.fr-mb-1w - .fr-mr-2w.no-wrap si le champ - .target.fr-mr-2w - = targeted_champ_tag - .operator.fr-mr-2w.no-wrap - = operator_tag - .value - = value_tag - .fr-hint-text - %span Si vous ne trouvez pas l'option correspondant à votre groupe, veuillez l'ajouter dans le champ dédié au - %span - = link_to 'routage', champs_admin_procedure_path(@procedure) - - .flex.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right + .flex.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right.mb-2 - if @groupe_instructeur.can_delete? %p= t('.delete') = button_to admin_procedure_groupe_instructeur_path(@procedure, @groupe_instructeur), diff --git a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml index b395f0816..73bf6a8aa 100644 --- a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml +++ b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml @@ -134,7 +134,7 @@ .type-de-champ-add-button{ id: dom_id(coordinate, :type_de_champ_add_button), class: class_names(hidden: !coordinate.empty?) } = render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?) - = render(TypesDeChampEditor::ConditionsComponent.new(tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ), procedure_id: procedure.id)) + = render(Conditions::ChampsConditionsComponent.new(tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ), procedure_id: procedure.id)) .type-de-champ-add-button{ class: class_names(root: !coordinate.child?) } = render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate&.parent, is_annotation: coordinate.private?, after_stable_id: type_de_champ.stable_id) diff --git a/spec/components/types_de_champ_editor/conditions_component_spec.rb b/spec/components/conditions/champs_conditions_component_spec.rb similarity index 99% rename from spec/components/types_de_champ_editor/conditions_component_spec.rb rename to spec/components/conditions/champs_conditions_component_spec.rb index ffce44546..acd73d0e9 100644 --- a/spec/components/types_de_champ_editor/conditions_component_spec.rb +++ b/spec/components/conditions/champs_conditions_component_spec.rb @@ -1,4 +1,4 @@ -describe TypesDeChampEditor::ConditionsComponent, type: :component do +describe Conditions::ChampsConditionsComponent, type: :component do include Logic describe 'render' do From 4d95f49c8288bf908a732661a9891a8c82ec1bae Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Fri, 13 Oct 2023 11:31:41 +0200 Subject: [PATCH 02/10] chore(conditions): update controllers and routes --- .../administrateurs/conditions_controller.rb | 4 +- .../administrateurs/routing_controller.rb | 56 ------- .../routing_rules_controller.rb | 79 +++++++++ config/routes.rb | 9 +- .../routing_controller_spec.rb | 112 ------------- .../routing_rules_controller_spec.rb | 153 ++++++++++++++++++ 6 files changed, 241 insertions(+), 172 deletions(-) delete mode 100644 app/controllers/administrateurs/routing_controller.rb create mode 100644 app/controllers/administrateurs/routing_rules_controller.rb delete mode 100644 spec/controllers/administrateurs/routing_controller_spec.rb create mode 100644 spec/controllers/administrateurs/routing_rules_controller_spec.rb diff --git a/app/controllers/administrateurs/conditions_controller.rb b/app/controllers/administrateurs/conditions_controller.rb index edf2c58db..30a443f21 100644 --- a/app/controllers/administrateurs/conditions_controller.rb +++ b/app/controllers/administrateurs/conditions_controller.rb @@ -42,7 +42,7 @@ module Administrateurs private def build_condition_component - TypesDeChampEditor::ConditionsComponent.new( + Conditions::ChampsConditionsComponent.new( tdc: @tdc, upper_tdcs: @upper_tdcs, procedure_id: @procedure.id @@ -50,7 +50,7 @@ module Administrateurs end def condition_form - ConditionForm.new(condition_params.merge({ upper_tdcs: @upper_tdcs })) + ConditionForm.new(condition_params.merge({ source_tdcs: @upper_tdcs })) end def retrieve_coordinate_and_uppers diff --git a/app/controllers/administrateurs/routing_controller.rb b/app/controllers/administrateurs/routing_controller.rb deleted file mode 100644 index 3e047cdbf..000000000 --- a/app/controllers/administrateurs/routing_controller.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Administrateurs - class RoutingController < AdministrateurController - include Logic - - before_action :retrieve_procedure - - def update - left = targeted_champ - - right = targeted_champ_changed? ? empty : value - - new_routing_rule = case operator_name - when Eq.name - ds_eq(left, right) - when NotEq.name - ds_not_eq(left, right) - end - groupe_instructeur.update!(routing_rule: new_routing_rule) - end - - def update_defaut_groupe_instructeur - new_defaut = @procedure.groupe_instructeurs.find(defaut_groupe_instructeur_id) - @procedure.update!(defaut_groupe_instructeur: new_defaut) - end - - private - - def targeted_champ_changed? - targeted_champ != groupe_instructeur.routing_rule&.left - end - - def targeted_champ - Logic.from_json(params[:targeted_champ]) - end - - def operator_name - params[:operator_name] - end - - def value - Logic.from_json(params[:value]) - end - - def groupe_instructeur - @groupe_instructeur ||= @procedure.groupe_instructeurs.find(groupe_instructeur_id) - end - - def groupe_instructeur_id - params[:groupe_instructeur_id] - end - - def defaut_groupe_instructeur_id - params[:defaut_groupe_instructeur_id] - end - end -end diff --git a/app/controllers/administrateurs/routing_rules_controller.rb b/app/controllers/administrateurs/routing_rules_controller.rb new file mode 100644 index 000000000..25b39c819 --- /dev/null +++ b/app/controllers/administrateurs/routing_rules_controller.rb @@ -0,0 +1,79 @@ +module Administrateurs + class RoutingRulesController < AdministrateurController + include Logic + before_action :retrieve_procedure, :retrieve_tdcs + before_action :retrieve_groupe_instructeur, except: [:update_defaut_groupe_instructeur] + + def update + condition = condition_form.to_condition + @groupe_instructeur.update!(routing_rule: condition) + + @routing_rule_component = build_routing_rule_component + end + + def add_row + condition = Logic.add_empty_condition_to(@groupe_instructeur.routing_rule) + @groupe_instructeur.update!(routing_rule: condition) + + @routing_rule_component = build_routing_rule_component + end + + def delete_row + condition = condition_form.delete_row(row_index).to_condition + @groupe_instructeur.update!(routing_rule: condition) + + @routing_rule_component = build_routing_rule_component + end + + def change_targeted_champ + condition = condition_form.change_champ(row_index).to_condition + @groupe_instructeur.update!(routing_rule: condition) + + @routing_rule_component = build_routing_rule_component + end + + def update_defaut_groupe_instructeur + new_defaut = @procedure.groupe_instructeurs.find(defaut_groupe_instructeur_id) + @procedure.update!(defaut_groupe_instructeur: new_defaut) + end + + private + + def groupe_instructeur_id + params[:groupe_instructeur_id] + end + + def defaut_groupe_instructeur_id + params[:defaut_groupe_instructeur_id] + end + + def build_routing_rule_component + Conditions::RoutingRulesComponent.new( + groupe_instructeur: @groupe_instructeur + ) + end + + def condition_form + ConditionForm.new(routing_rule_params.merge({ source_tdcs: @source_tdcs })) + end + + def retrieve_tdcs + @source_tdcs = @procedure.active_revision.types_de_champ + end + + def retrieve_groupe_instructeur + @groupe_instructeur = @procedure.groupe_instructeurs.find(groupe_instructeur_id) + end + + def routing_rule_params + params + .require(:groupe_instructeur) + .require(:condition_form) + .permit(:top_operator_name, rows: [:targeted_champ, :operator_name, :value]) + end + + def row_index + params[:row_index].to_i + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 4443b6525..acae04d68 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -546,8 +546,13 @@ Rails.application.routes.draw do delete :delete_row, on: :member end - patch :update, controller: 'routing', as: :routing_rules - patch :update_defaut_groupe_instructeur, controller: 'routing', as: :update_defaut_groupe_instructeur + resources :routing_rules, only: [:update, :destroy], param: :groupe_instructeur_id do + patch :add_row, on: :member + patch :change_targeted_champ, on: :member + delete :delete_row, on: :member + end + + patch :update_defaut_groupe_instructeur, controller: 'routing_rules', as: :update_defaut_groupe_instructeur put 'clone' put 'archive' diff --git a/spec/controllers/administrateurs/routing_controller_spec.rb b/spec/controllers/administrateurs/routing_controller_spec.rb deleted file mode 100644 index 17eff6874..000000000 --- a/spec/controllers/administrateurs/routing_controller_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -describe Administrateurs::RoutingController, type: :controller do - include Logic - - before { sign_in(procedure.administrateurs.first.user) } - - describe '#update targeted champ' do - let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }, { type: :text, libelle: 'Un champ texte' }]) } - let(:gi_2) { create(:groupe_instructeur, label: 'groupe 2', procedure: procedure) } - let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first } - let(:params) do - { - procedure_id: procedure.id, - targeted_champ: champ_value(drop_down_tdc.stable_id).to_json, - operator_name: operator_name, - value: empty.to_json, - groupe_instructeur_id: gi_2.id - } - end - - context 'with Eq operator' do - let(:operator_name) { Logic::Eq.name } - - before do - post :update, params: params, format: :turbo_stream - end - - it do - expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), empty)) - end - - context '#update value' do - let(:value_updated_params) { params.merge(value: constant('Lyon').to_json) } - - before do - post :update, params: value_updated_params, format: :turbo_stream - end - - it do - expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon'))) - end - - context 'targeted champ changed' do - let(:last_tdc) { procedure.draft_revision.types_de_champ.last } - - before do - targeted_champ_updated_params = value_updated_params.merge(targeted_champ: champ_value(last_tdc.stable_id).to_json) - post :update, params: targeted_champ_updated_params, format: :turbo_stream - end - - it do - expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(last_tdc.stable_id), empty)) - end - end - end - end - - context 'with NotEq operator' do - let(:operator_name) { Logic::NotEq.name } - - before do - post :update, params: params, format: :turbo_stream - end - - it do - expect(gi_2.reload.routing_rule).to eq(ds_not_eq(champ_value(drop_down_tdc.stable_id), empty)) - end - - context '#update value' do - let(:value_updated_params) { params.merge(value: constant('Lyon').to_json) } - - before do - post :update, params: value_updated_params, format: :turbo_stream - end - - it do - expect(gi_2.reload.routing_rule).to eq(ds_not_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon'))) - end - - context 'targeted champ changed' do - let(:last_tdc) { procedure.draft_revision.types_de_champ.last } - - before do - targeted_champ_updated_params = value_updated_params.merge(targeted_champ: champ_value(last_tdc.stable_id).to_json) - post :update, params: targeted_champ_updated_params, format: :turbo_stream - end - - it do - expect(gi_2.reload.routing_rule).to eq(ds_not_eq(champ_value(last_tdc.stable_id), empty)) - end - end - end - end - end - - describe "#update_defaut_groupe_instructeur" do - let(:procedure) { create(:procedure) } - let(:gi_2) { create(:groupe_instructeur, label: 'groupe 2', procedure: procedure) } - let(:params) do - { - procedure_id: procedure.id, - defaut_groupe_instructeur_id: gi_2.id - } - end - - before do - post :update_defaut_groupe_instructeur, params: params, format: :turbo_stream - procedure.reload - end - - it { expect(procedure.defaut_groupe_instructeur.id).to eq(gi_2.id) } - end -end diff --git a/spec/controllers/administrateurs/routing_rules_controller_spec.rb b/spec/controllers/administrateurs/routing_rules_controller_spec.rb new file mode 100644 index 000000000..39169fe3a --- /dev/null +++ b/spec/controllers/administrateurs/routing_rules_controller_spec.rb @@ -0,0 +1,153 @@ +describe Administrateurs::RoutingRulesController, type: :controller do + include Logic + + before { sign_in(procedure.administrateurs.first.user) } + + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }, { type: :text, libelle: 'Un champ texte' }]) } + let(:gi_2) { create(:groupe_instructeur, label: 'groupe 2', procedure: procedure) } + let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first } + let(:default_params) do + { + procedure_id: procedure.id, + groupe_instructeur_id: gi_2.id + } + end + + describe '#update' do + let(:value) { empty.to_json } + let(:targeted_champ) { champ_value(drop_down_tdc.stable_id).to_json } + + before { post :update, params: params, format: :turbo_stream } + + let(:params) { default_params.merge(groupe_instructeur: { condition_form: condition_form }) } + + let(:condition_form) do + { + rows: [ + { + targeted_champ: targeted_champ, + operator_name: operator_name, + value: value + } + ] + } + end + + context 'with Eq operator' do + let(:operator_name) { Logic::Eq.name } + it do + expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), empty)) + end + + context '#update value' do + let(:value) { constant('Lyon').to_json } + + before { post :update, params: params, format: :turbo_stream } + + it do + expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon'))) + end + + context 'targeted champ changed' do + let(:last_tdc) { procedure.draft_revision.types_de_champ.last } + let(:targeted_champ) { champ_value(last_tdc.stable_id).to_json } + let(:value) { empty.to_json } + + before { post :update, params: params, format: :turbo_stream } + + it do + expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(last_tdc.stable_id), empty)) + end + end + end + end + + context 'with NotEq operator' do + let(:operator_name) { Logic::NotEq.name } + it do + expect(gi_2.reload.routing_rule).to eq(ds_not_eq(champ_value(drop_down_tdc.stable_id), empty)) + end + + context '#update value' do + let(:value) { constant('Lyon').to_json } + + before { post :update, params: params, format: :turbo_stream } + + it do + expect(gi_2.reload.routing_rule).to eq(ds_not_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon'))) + end + + context 'targeted champ changed' do + let(:last_tdc) { procedure.draft_revision.types_de_champ.last } + let(:targeted_champ) { champ_value(last_tdc.stable_id).to_json } + let(:value) { empty.to_json } + + before { post :update, params: params, format: :turbo_stream } + + it do + expect(gi_2.reload.routing_rule).to eq(ds_not_eq(champ_value(last_tdc.stable_id), empty)) + end + end + end + end + end + + describe '#add_row' do + before do + gi_2.update(routing_rule: ds_eq(champ_value(drop_down_tdc.stable_id), empty)) + post :add_row, params: default_params, format: :turbo_stream + end + + it do + expect(gi_2.reload.routing_rule).to eq(ds_and([ds_eq(champ_value(drop_down_tdc.stable_id), empty), empty_operator(empty, empty)])) + end + end + + describe '#delete_row' do + before do + gi_2.update(routing_rule: ds_and([ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon')), empty_operator(empty, empty)])) + post :delete_row, params: params.merge(row_index: 1), format: :turbo_stream + end + + let(:params) { default_params.merge(groupe_instructeur: { condition_form: condition_form }) } + + let(:condition_form) do + { + rows: [ + { + targeted_champ: champ_value(drop_down_tdc.stable_id).to_json, + operator_name: Logic::Eq.name, + value: constant('Lyon') + }, + { + targeted_champ: empty, + operator_name: Logic::Eq.name, + value: empty + } + ] + } + end + + it do + expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon'))) + end + end + + describe "#update_defaut_groupe_instructeur" do + let(:procedure) { create(:procedure) } + let(:gi_2) { create(:groupe_instructeur, label: 'groupe 2', procedure: procedure) } + let(:params) do + { + procedure_id: procedure.id, + defaut_groupe_instructeur_id: gi_2.id + } + end + + before do + post :update_defaut_groupe_instructeur, params: params, format: :turbo_stream + procedure.reload + end + + it { expect(procedure.defaut_groupe_instructeur.id).to eq(gi_2.id) } + end +end From 28b07f7650d5fd69badb074b1a5495808b2ca30a Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Fri, 13 Oct 2023 11:30:53 +0200 Subject: [PATCH 03/10] chore(conditions): update condition_form --- app/models/condition_form.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/condition_form.rb b/app/models/condition_form.rb index ea103a8de..1fe815dfe 100644 --- a/app/models/condition_form.rb +++ b/app/models/condition_form.rb @@ -2,7 +2,7 @@ class ConditionForm include ActiveModel::Model include Logic - attr_accessor :top_operator_name, :rows, :upper_tdcs + attr_accessor :top_operator_name, :rows, :source_tdcs def to_condition case sub_conditions.count @@ -22,7 +22,7 @@ class ConditionForm end def change_champ(i) - sub_conditions[i] = Logic.ensure_compatibility_from_left(sub_conditions[i], upper_tdcs) + sub_conditions[i] = Logic.ensure_compatibility_from_left(sub_conditions[i], source_tdcs) self end @@ -39,7 +39,7 @@ class ConditionForm def row_to_condition(row) left = Logic.from_json(row[:targeted_champ]) - right = parse_value(left.type(upper_tdcs), row[:value]) + right = parse_value(left.type(source_tdcs), row[:value]) Logic.class_from_name(row[:operator_name]).new(left, right) end From 22feb48c29e02bc38fd259e7267e44d72cd8ac82 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Fri, 13 Oct 2023 11:32:15 +0200 Subject: [PATCH 04/10] chore(conditions): update views --- .../routing_rules/_update.turbo_stream.haml | 7 +++++++ .../routing_rules/add_row.turbo_stream.haml | 1 + .../routing_rules/change_targeted_champ.turbo_stream.haml | 1 + .../routing_rules/delete_row.turbo_stream.haml | 1 + .../routing_rules/destroy.turbo_stream.haml | 1 + .../{routing => routing_rules}/update.turbo_stream.haml | 0 6 files changed, 11 insertions(+) create mode 100644 app/views/administrateurs/routing_rules/_update.turbo_stream.haml create mode 100644 app/views/administrateurs/routing_rules/add_row.turbo_stream.haml create mode 100644 app/views/administrateurs/routing_rules/change_targeted_champ.turbo_stream.haml create mode 100644 app/views/administrateurs/routing_rules/delete_row.turbo_stream.haml create mode 100644 app/views/administrateurs/routing_rules/destroy.turbo_stream.haml rename app/views/administrateurs/{routing => routing_rules}/update.turbo_stream.haml (100%) diff --git a/app/views/administrateurs/routing_rules/_update.turbo_stream.haml b/app/views/administrateurs/routing_rules/_update.turbo_stream.haml new file mode 100644 index 000000000..7e7275718 --- /dev/null +++ b/app/views/administrateurs/routing_rules/_update.turbo_stream.haml @@ -0,0 +1,7 @@ +- rendered = render @routing_rule_component + +- if rendered.present? + = turbo_stream.replace dom_id(@groupe_instructeur, :routing_rule) do + - rendered +- else + = turbo_stream.remove dom_id(@groupe_instructeur, :routing_rule) diff --git a/app/views/administrateurs/routing_rules/add_row.turbo_stream.haml b/app/views/administrateurs/routing_rules/add_row.turbo_stream.haml new file mode 100644 index 000000000..8f9900e50 --- /dev/null +++ b/app/views/administrateurs/routing_rules/add_row.turbo_stream.haml @@ -0,0 +1 @@ += render partial: 'update' diff --git a/app/views/administrateurs/routing_rules/change_targeted_champ.turbo_stream.haml b/app/views/administrateurs/routing_rules/change_targeted_champ.turbo_stream.haml new file mode 100644 index 000000000..8f9900e50 --- /dev/null +++ b/app/views/administrateurs/routing_rules/change_targeted_champ.turbo_stream.haml @@ -0,0 +1 @@ += render partial: 'update' diff --git a/app/views/administrateurs/routing_rules/delete_row.turbo_stream.haml b/app/views/administrateurs/routing_rules/delete_row.turbo_stream.haml new file mode 100644 index 000000000..8f9900e50 --- /dev/null +++ b/app/views/administrateurs/routing_rules/delete_row.turbo_stream.haml @@ -0,0 +1 @@ += render partial: 'update' diff --git a/app/views/administrateurs/routing_rules/destroy.turbo_stream.haml b/app/views/administrateurs/routing_rules/destroy.turbo_stream.haml new file mode 100644 index 000000000..8f9900e50 --- /dev/null +++ b/app/views/administrateurs/routing_rules/destroy.turbo_stream.haml @@ -0,0 +1 @@ += render partial: 'update' diff --git a/app/views/administrateurs/routing/update.turbo_stream.haml b/app/views/administrateurs/routing_rules/update.turbo_stream.haml similarity index 100% rename from app/views/administrateurs/routing/update.turbo_stream.haml rename to app/views/administrateurs/routing_rules/update.turbo_stream.haml From cf80fd03e19ad331db311869d0390ef508959f5f Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Fri, 13 Oct 2023 15:00:07 +0200 Subject: [PATCH 05/10] chore(routing): update routing_rule validations --- .../routing_rules_component.html.haml | 6 ++++- app/models/groupe_instructeur.rb | 27 ++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml index 830e17526..286f7508d 100644 --- a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml +++ b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml @@ -4,7 +4,11 @@ data: { turbo: true, controller: 'autosave' }, class: 'form width-100' do .conditionnel.mt-2.width-100 - + .flex + - if @groupe_instructeur.invalid_rule? + %p.fr-mb-1w.fr-badge.fr-badge--warning.fr-badge--sm règle invalide + - elsif @groupe_instructeur.non_unique_rule? + %p.fr-mb-1w.fr-badge.fr-badge--warning.fr-badge--sm règle déjà attribuée à #{@groupe_instructeur.groups_with_same_rule} %table.condition-table.mt-2 %thead %tr diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index 034e02dc2..7c937f088 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -84,7 +84,15 @@ class GroupeInstructeur < ApplicationRecord def valid_rule? return false if routing_rule.nil? - ([routing_rule.left, routing_rule, routing_rule.right] in [ChampValue, Eq | NotEq, Constant]) && routing_rule_matches_tdc? + if [And, Or].include?(routing_rule.class) + routing_rule.operands.all? { |rule_line| valid_rule_line?(rule_line) } + else + valid_rule_line?(routing_rule) + end + end + + def valid_rule_line?(rule) + ([rule.left, rule, rule.right] in [ChampValue, (LessThan | LessThanEq | Eq | NotEq | GreaterThanEq | GreaterThan | IncludeOperator), Constant]) && routing_rule_matches_tdc?(rule) end def non_unique_rule? @@ -95,7 +103,8 @@ class GroupeInstructeur < ApplicationRecord def groups_with_same_rule return if routing_rule.nil? other_groupe_instructeurs - .filter { |gi| !gi.routing_rule.nil? && gi.routing_rule.right != empty && gi.routing_rule == routing_rule } + .filter { _1.routing_rule.present? } + .filter { _1.routing_rule == routing_rule } .map(&:label) .join(', ') end @@ -106,18 +115,24 @@ class GroupeInstructeur < ApplicationRecord private - def routing_rule_matches_tdc? - routing_tdc = procedure.active_revision.types_de_champ.find_by(stable_id: routing_rule.left.stable_id) + def routing_rule_matches_tdc?(rule) + routing_tdc = procedure.active_revision.types_de_champ.find_by(stable_id: rule.left.stable_id) options = case routing_tdc.type_champ when TypeDeChamp.type_champs.fetch(:communes), TypeDeChamp.type_champs.fetch(:departements), TypeDeChamp.type_champs.fetch(:epci) APIGeoService.departements.map { _1[:code] } when TypeDeChamp.type_champs.fetch(:regions) APIGeoService.regions.map { _1[:code] } - when TypeDeChamp.type_champs.fetch(:drop_down_list) + when TypeDeChamp.type_champs.fetch(:drop_down_list), TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) routing_tdc.drop_down_list_enabled_non_empty_options(other: true).map { _1.is_a?(Array) ? _1.last : _1 } + when TypeDeChamp.type_champs.fetch(:checkbox), TypeDeChamp.type_champs.fetch(:yes_no) + [true, false] + when TypeDeChamp.type_champs.fetch(:integer_number) + return rule.right.value.is_a? Integer + when TypeDeChamp.type_champs.fetch(:decimal_number) + return rule.right.value.is_a? Float end - routing_rule.right.value.in?(options) + rule.right.value.in?(options) end serialize :routing_rule, LogicSerializer From 469a50f19d72dc0aa2dab917188f43168dd24c64 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Tue, 17 Oct 2023 17:38:51 +0200 Subject: [PATCH 06/10] refactor(condition): move conditions_error_component --- .../champs_conditions_component.html.haml | 2 +- .../conditions_errors_component.rb | 16 ++++++------- .../conditions_errors_component.en.yml | 0 .../conditions_errors_component.fr.yml | 0 .../conditions_errors_component.html.haml | 0 .../routing_rules_component.html.haml | 3 +++ .../conditions_errors_component_spec.rb | 24 +++++++++---------- 7 files changed, 24 insertions(+), 21 deletions(-) rename app/components/{types_de_champ_editor => conditions}/conditions_errors_component.rb (80%) rename app/components/{types_de_champ_editor => conditions}/conditions_errors_component/conditions_errors_component.en.yml (100%) rename app/components/{types_de_champ_editor => conditions}/conditions_errors_component/conditions_errors_component.fr.yml (100%) rename app/components/{types_de_champ_editor => conditions}/conditions_errors_component/conditions_errors_component.html.haml (100%) diff --git a/app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml b/app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml index f9e45c340..aa19e3b26 100644 --- a/app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml +++ b/app/components/conditions/champs_conditions_component/champs_conditions_component.html.haml @@ -6,7 +6,7 @@ Logique conditionnelle = logic_conditionnel_button - = render TypesDeChampEditor::ConditionsErrorsComponent.new(conditions: condition_per_row, upper_tdcs: @source_tdcs) + = render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs) - if @condition.present? %table.condition-table.mt-2.width-100 diff --git a/app/components/types_de_champ_editor/conditions_errors_component.rb b/app/components/conditions/conditions_errors_component.rb similarity index 80% rename from app/components/types_de_champ_editor/conditions_errors_component.rb rename to app/components/conditions/conditions_errors_component.rb index d3500ab75..335716c97 100644 --- a/app/components/types_de_champ_editor/conditions_errors_component.rb +++ b/app/components/conditions/conditions_errors_component.rb @@ -1,13 +1,13 @@ -class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent - def initialize(conditions:, upper_tdcs:) - @conditions, @upper_tdcs = conditions, upper_tdcs +class Conditions::ConditionsErrorsComponent < ApplicationComponent + def initialize(conditions:, source_tdcs:) + @conditions, @source_tdcs = conditions, source_tdcs end private def errors errors = @conditions - .flat_map { |condition| condition.errors(@upper_tdcs) } + .flat_map { |condition| condition.errors(@source_tdcs) } .uniq # if a tdc is not available (has been removed for example) @@ -34,13 +34,13 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent in { type: :incompatible, stable_id: nil } t('not_available', scope: '.errors') in { type: :unmanaged, stable_id: stable_id } - targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id } + targeted_champ = @source_tdcs.find { |tdc| tdc.stable_id == stable_id } t('unmanaged', scope: '.errors', libelle: targeted_champ.libelle, type_champ: t(targeted_champ.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')&.downcase) in { type: :incompatible, stable_id: stable_id, right: right, operator_name: operator_name } - targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id } + targeted_champ = @source_tdcs.find { |tdc| tdc.stable_id == stable_id } t('incompatible', scope: '.errors', libelle: targeted_champ.libelle, type_champ: t(targeted_champ.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')&.downcase, @@ -50,7 +50,7 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent t('required_number', scope: '.errors', operator: t(operator_name, scope: 'logic.operators')) in { type: :not_included, stable_id: stable_id, right: right } - targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id } + targeted_champ = @source_tdcs.find { |tdc| tdc.stable_id == stable_id } t('not_included', scope: '.errors', libelle: targeted_champ.libelle, right: right.to_s.downcase) @@ -67,7 +67,7 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent def render? @conditions - .filter { |condition| condition.errors(@upper_tdcs).present? } + .filter { |condition| condition.errors(@source_tdcs).present? } .present? end end diff --git a/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.en.yml b/app/components/conditions/conditions_errors_component/conditions_errors_component.en.yml similarity index 100% rename from app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.en.yml rename to app/components/conditions/conditions_errors_component/conditions_errors_component.en.yml diff --git a/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.fr.yml b/app/components/conditions/conditions_errors_component/conditions_errors_component.fr.yml similarity index 100% rename from app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.fr.yml rename to app/components/conditions/conditions_errors_component/conditions_errors_component.fr.yml diff --git a/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.html.haml b/app/components/conditions/conditions_errors_component/conditions_errors_component.html.haml similarity index 100% rename from app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.html.haml rename to app/components/conditions/conditions_errors_component/conditions_errors_component.html.haml diff --git a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml index 286f7508d..9830d9864 100644 --- a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml +++ b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml @@ -9,6 +9,9 @@ %p.fr-mb-1w.fr-badge.fr-badge--warning.fr-badge--sm règle invalide - elsif @groupe_instructeur.non_unique_rule? %p.fr-mb-1w.fr-badge.fr-badge--warning.fr-badge--sm règle déjà attribuée à #{@groupe_instructeur.groups_with_same_rule} + + = render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs) + %table.condition-table.mt-2 %thead %tr diff --git a/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb b/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb index e829bc305..8bc2ad6e6 100644 --- a/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb +++ b/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb @@ -1,11 +1,11 @@ -describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do +describe Conditions::ConditionsErrorsComponent, type: :component do include Logic describe 'render' do let(:conditions) { [] } - let(:upper_tdcs) { [] } + let(:source_tdcs) { [] } - before { render_inline(described_class.new(conditions: conditions, upper_tdcs: upper_tdcs)) } + before { render_inline(described_class.new(conditions: conditions, source_tdcs: source_tdcs)) } context 'when there are no condition' do it { expect(page).to have_no_css('.errors-summary') } @@ -23,7 +23,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when the targeted_champ is unmanaged' do let(:tdc) { create(:type_de_champ_address) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] } it do @@ -34,7 +34,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when the types mismatch' do let(:tdc) { create(:type_de_champ_integer_number) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant('a'))] } it { expect(page).to have_content("Le champ « #{tdc.libelle} » est de type « nombre entier ». Il ne peut pas être égal à « a ».") } @@ -42,7 +42,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when a number operator is applied on not a number' do let(:tdc) { create(:type_de_champ_multiple_drop_down_list) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [greater_than(champ_value(tdc.stable_id), constant('a text'))] } it { expect(page).to have_content("« Supérieur à » ne s'applique qu'à des nombres.") } @@ -50,7 +50,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when the include operator is applied on a list' do let(:tdc) { create(:type_de_champ_integer_number) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_include(champ_value(tdc.stable_id), constant('a text'))] } it { expect(page).to have_content("Lʼopérateur « inclus » ne s'applique qu'au choix simple ou multiple.") } @@ -58,7 +58,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when a choice is not in a drop_down' do let(:tdc) { create(:type_de_champ_drop_down_list) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant('another choice'))] } it { expect(page).to have_content("« another choice » ne fait pas partie de « #{tdc.libelle} ».") } @@ -66,7 +66,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when a choice is not in a multiple_drop_down' do let(:tdc) { create(:type_de_champ_multiple_drop_down_list) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_include(champ_value(tdc.stable_id), constant('another choice'))] } it { expect(page).to have_content("« another choice » ne fait pas partie de « #{tdc.libelle} ».") } @@ -74,7 +74,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when an eq operator applies to a multiple_drop_down' do let(:tdc) { create(:type_de_champ_multiple_drop_down_list) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(tdc.drop_down_list_enabled_non_empty_options.first))] } it { expect(page).to have_content("« est » ne s'applique pas au choix multiple.") } @@ -82,7 +82,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do context 'when an not_eq operator applies to a multiple_drop_down' do let(:tdc) { create(:type_de_champ_multiple_drop_down_list) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_not_eq(champ_value(tdc.stable_id), constant(tdc.drop_down_list_enabled_non_empty_options.first))] } it { expect(page).to have_content("« n’est pas » ne s'applique pas au choix multiple.") } @@ -92,7 +92,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do # Cf https://demarches-simplifiees.sentry.io/issues/3625488398/events/53164e105bc94d55a004d69f96d58fb2/?project=1429550 # However maybe we should not have empty at left with still a constant at right let(:tdc) { create(:type_de_champ_integer_number) } - let(:upper_tdcs) { [tdc] } + let(:source_tdcs) { [tdc] } let(:conditions) { [ds_eq(empty, constant('a text'))] } it { expect(page).to have_content("Un champ cible n'est plus disponible") } From cd9cc65e990c441e601e58f209a8900ea067cde2 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Tue, 17 Oct 2023 19:02:51 +0200 Subject: [PATCH 07/10] feat(routing): add a feature flag on multi line routing --- .../routing_rules_component/routing_rules_component.html.haml | 4 ++-- config/initializers/flipper.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml index 9830d9864..6ea962d59 100644 --- a/app/components/conditions/routing_rules_component/routing_rules_component.html.haml +++ b/app/components/conditions/routing_rules_component/routing_rules_component.html.haml @@ -28,5 +28,5 @@ %td.operator= operator_tag(operator_name, targeted_champ, row_index) %td.value= right_operand_tag(targeted_champ, value, row_index) %td.delete-column= delete_condition_tag(row_index) - - .flex.justify-end.mt-2= add_condition_tag + - if @groupe_instructeur.procedure.feature_enabled?(:multi_line_routing) + .flex.justify-end.mt-2= add_condition_tag diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index f7cbd99dc..ae0436952 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -20,7 +20,8 @@ features = [ :groupe_instructeur_api_hack, :rerouting, :cojo_type_de_champ, - :sva + :sva, + :multi_line_routing ] def database_exists? From 3e2e5a01f9fb34e3690a1e65ad0b1388fb9e47c0 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Wed, 18 Oct 2023 18:44:52 +0200 Subject: [PATCH 08/10] fix(test): update routing tests --- .../routing_rules_component_spec.rb | 52 +++++++++++++++++++ .../groupe_instructeurs_controller_spec.rb | 14 ++--- .../routing/rules_full_scenario_spec.rb | 20 +++---- 3 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 spec/components/conditions/routing_rules_component_spec.rb diff --git a/spec/components/conditions/routing_rules_component_spec.rb b/spec/components/conditions/routing_rules_component_spec.rb new file mode 100644 index 000000000..f6fcb5d1d --- /dev/null +++ b/spec/components/conditions/routing_rules_component_spec.rb @@ -0,0 +1,52 @@ +describe Conditions::RoutingRulesComponent, type: :component do + include Logic + + describe 'render' do + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }, { type: :integer_number, libelle: 'Un champ nombre entier' }]) } + let(:groupe_instructeur) { procedure.groupe_instructeurs.first } + let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first } + let(:integer_number_tdc) { procedure.draft_revision.types_de_champ.last } + let(:routing_rule) { ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon')) } + + before do + groupe_instructeur.update(routing_rule: routing_rule) + render_inline(described_class.new(groupe_instructeur: groupe_instructeur)) + end + + context 'with one row' do + context 'when routing rule is valid' do + it do + expect(page).to have_text('Champ Cible') + expect(page).not_to have_text('règle invalide') + expect(page).to have_select('groupe_instructeur[condition_form][rows][][operator_name]', options: ["Est", "N’est pas"]) + end + end + + context 'when routing rule is invalid' do + let(:routing_rule) { ds_eq(champ_value(drop_down_tdc.stable_id), empty) } + it { expect(page).to have_text('règle invalide') } + end + end + + context 'with two rows' do + context 'when routing rule is valid' do + let(:routing_rule) { ds_and([ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon')), ds_not_eq(champ_value(integer_number_tdc.stable_id), constant(33))]) } + + it do + expect(page).not_to have_text('règle invalide') + expect(page).to have_selector('tbody > tr', count: 2) + expect(page).to have_select("groupe_instructeur_condition_form_top_operator_name", selected: "Et", options: ['Et', 'Ou']) + end + end + + context 'when routing rule is invalid' do + let(:routing_rule) { ds_or([ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon')), ds_not_eq(champ_value(integer_number_tdc.stable_id), empty)]) } + it do + expect(page).to have_text('règle invalide') + expect(page).to have_selector('tbody > tr', count: 2) + expect(page).to have_select("groupe_instructeur_condition_form_top_operator_name", selected: "Ou", options: ['Et', 'Ou']) + end + end + end + end +end diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index c6737c284..a87df3f8c 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -60,26 +60,18 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do end end - context 'group without routing rule' do - before { get :show, params: { procedure_id: procedure.id, id: gi_1_1.id } } - - it do - expect(response).to have_http_status(:ok) - expect(response.body).to include('règle invalide') - end - end - context 'group with routing rule matching tdc' do let!(:drop_down_tdc) { create(:type_de_champ_drop_down_list, procedure: procedure, drop_down_options: options) } - let(:options) { procedure.groupe_instructeurs.pluck(:label) } + let(:options) { ['Premier choix', 'Deuxième choix', 'Troisième choix'] } before do - gi_1_1.update(routing_rule: ds_eq(champ_value(drop_down_tdc.stable_id), constant(gi_1_1.label))) + gi_1_1.update(routing_rule: ds_eq(champ_value(drop_down_tdc.stable_id), constant('Deuxième choix'))) get :show, params: { procedure_id: procedure.id, id: gi_1_1.id } end it do expect(response).to have_http_status(:ok) + expect(response.body).to include('Deuxième choix') expect(response.body).not_to include('règle invalide') end end diff --git a/spec/system/routing/rules_full_scenario_spec.rb b/spec/system/routing/rules_full_scenario_spec.rb index f8f5414ec..886e0db5d 100644 --- a/spec/system/routing/rules_full_scenario_spec.rb +++ b/spec/system/routing/rules_full_scenario_spec.rb @@ -39,14 +39,14 @@ describe 'The routing with rules', js: true, retry: 3 do expect(page).not_to have_text('à configurer') click_on 'littéraire' - expect(page).to have_select("targeted_champ", selected: "Spécialité") - expect(page).to have_select("value", selected: "littéraire") + expect(page).to have_select("groupe_instructeur[condition_form][rows][][targeted_champ]", selected: "Spécialité") + expect(page).to have_select("groupe_instructeur[condition_form][rows][][value]", selected: "littéraire") click_on '3 groupes' click_on 'scientifique' - expect(page).to have_select("targeted_champ", selected: "Spécialité") - expect(page).to have_select("value", selected: "scientifique") + expect(page).to have_select("groupe_instructeur[condition_form][rows][][targeted_champ]", selected: "Spécialité") + expect(page).to have_select("groupe_instructeur[condition_form][rows][][value]", selected: "scientifique") end scenario 'Routage avancé' do @@ -106,20 +106,20 @@ describe 'The routing with rules', js: true, retry: 3 do expect(page).to have_text("L’instructeur alain@gouv.fr a été affecté") # add routing rules - within('.target') { select('Spécialité') } - within('.value') { select('scientifique') } + within('.target select') { select('Spécialité') } + within('.value select') { select('scientifique') } click_on '3 groupes' click_on 'littéraire' - within('.target') { select('Spécialité') } - within('.value') { select('scientifique') } + within('.target select') { select('Spécialité') } + within('.value select') { select('scientifique') } expect(page).to have_text('règle déjà attribuée à scientifique') - within('.target') { select('Spécialité') } - within('.value') { select('littéraire') } + within('.target select') { select('Spécialité') } + within('.value select') { select('littéraire') } expect(page).not_to have_text('règle déjà attribuée à scientifique') From 6c959d9d4e53003fae63b091f62bac0e4455e912 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Mon, 6 Nov 2023 17:44:21 +0100 Subject: [PATCH 09/10] refactor(groupe instructeur): use logic validation system for routing rule" --- app/models/groupe_instructeur.rb | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index 7c937f088..e9b67ac6c 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -116,23 +116,8 @@ class GroupeInstructeur < ApplicationRecord private def routing_rule_matches_tdc?(rule) - routing_tdc = procedure.active_revision.types_de_champ.find_by(stable_id: rule.left.stable_id) - - options = case routing_tdc.type_champ - when TypeDeChamp.type_champs.fetch(:communes), TypeDeChamp.type_champs.fetch(:departements), TypeDeChamp.type_champs.fetch(:epci) - APIGeoService.departements.map { _1[:code] } - when TypeDeChamp.type_champs.fetch(:regions) - APIGeoService.regions.map { _1[:code] } - when TypeDeChamp.type_champs.fetch(:drop_down_list), TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) - routing_tdc.drop_down_list_enabled_non_empty_options(other: true).map { _1.is_a?(Array) ? _1.last : _1 } - when TypeDeChamp.type_champs.fetch(:checkbox), TypeDeChamp.type_champs.fetch(:yes_no) - [true, false] - when TypeDeChamp.type_champs.fetch(:integer_number) - return rule.right.value.is_a? Integer - when TypeDeChamp.type_champs.fetch(:decimal_number) - return rule.right.value.is_a? Float - end - rule.right.value.in?(options) + tdcs = procedure.active_revision.types_de_champ_public + rule.errors(tdcs).blank? end serialize :routing_rule, LogicSerializer From 2465e13504d7657a6d998718fee10cffd20d9db9 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Mon, 6 Nov 2023 18:10:08 +0100 Subject: [PATCH 10/10] chore(logic): add a translation for empty rule --- app/models/logic/empty_operator.rb | 2 +- config/locales/models/logic/en.yml | 1 + config/locales/models/logic/fr.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/logic/empty_operator.rb b/app/models/logic/empty_operator.rb index 318229836..5315f0b87 100644 --- a/app/models/logic/empty_operator.rb +++ b/app/models/logic/empty_operator.rb @@ -1,5 +1,5 @@ class Logic::EmptyOperator < Logic::BinaryOperator - def to_s(_type_de_champs = []) = "empty operator" + def to_s(_type_de_champs = []) = I18n.t('logic.empty_operator') def type(_type_de_champs = []) = :empty diff --git a/config/locales/models/logic/en.yml b/config/locales/models/logic/en.yml index 198f0e4e9..c045730ff 100644 --- a/config/locales/models/logic/en.yml +++ b/config/locales/models/logic/en.yml @@ -1,3 +1,4 @@ en: logic: empty: empty member + empty_operator: empty operator diff --git a/config/locales/models/logic/fr.yml b/config/locales/models/logic/fr.yml index 27688d67f..7f3045c3d 100644 --- a/config/locales/models/logic/fr.yml +++ b/config/locales/models/logic/fr.yml @@ -1,6 +1,7 @@ fr: logic: empty: un membre vide + empty_operator: un opérateur vide is: Est is_not: N’est pas operators: