diff --git a/app/assets/stylesheets/conditions_component.scss b/app/assets/stylesheets/conditions_component.scss
index 055e9b4f9..59f8bf6b9 100644
--- a/app/assets/stylesheets/conditions_component.scss
+++ b/app/assets/stylesheets/conditions_component.scss
@@ -57,5 +57,14 @@ form.form > .conditionnel {
select.alert {
border-color: $dark-red;
}
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &:last-child {
+ text-align: right;
+ padding-right: 0;
+ }
}
}
diff --git a/app/components/conditions/conditions_component.rb b/app/components/conditions/conditions_component.rb
index 01f081a85..8817a1336 100644
--- a/app/components/conditions/conditions_component.rb
+++ b/app/components/conditions/conditions_component.rb
@@ -61,7 +61,7 @@ class Conditions::ConditionsComponent < ApplicationComponent
def available_targets_for_select
@source_tdcs
- .filter { |tdc| ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(tdc.type_champ) }
+ .filter(&:conditionable?)
.map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
end
diff --git a/app/components/conditions/ineligibilite_rules_component.rb b/app/components/conditions/ineligibilite_rules_component.rb
new file mode 100644
index 000000000..a12ab262e
--- /dev/null
+++ b/app/components/conditions/ineligibilite_rules_component.rb
@@ -0,0 +1,34 @@
+class Conditions::IneligibiliteRulesComponent < Conditions::ConditionsComponent
+ include Logic
+
+ def initialize(draft_revision:)
+ @draft_revision = draft_revision
+ @published_revision = draft_revision.procedure.published_revision
+ @condition = draft_revision.ineligibilite_rules
+ @source_tdcs = draft_revision.types_de_champ_for(scope: :public)
+ end
+
+ def pending_changes?
+ return false if !@published_revision
+
+ !@published_revision.compare_ineligibilite_rules(@draft_revision).empty?
+ end
+
+ private
+
+ def input_prefix
+ 'procedure_revision[condition_form]'
+ end
+
+ def input_id_for(name, row_index)
+ "#{@draft_revision.id}-#{name}-#{row_index}"
+ end
+
+ def delete_condition_path(row_index)
+ delete_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id, row_index:)
+ end
+
+ def add_condition_path
+ add_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id)
+ end
+end
diff --git a/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.fr.yml b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.fr.yml
new file mode 100644
index 000000000..b646c3019
--- /dev/null
+++ b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.fr.yml
@@ -0,0 +1,6 @@
+---
+fr:
+ display_if: Bloquer si
+ select: Sélectionner
+ add_condition: Ajouter une règle d’inéligibilité
+ remove_a_row: Supprimer une règle
diff --git a/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.html.haml b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.html.haml
new file mode 100644
index 000000000..313618167
--- /dev/null
+++ b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.html.haml
@@ -0,0 +1,49 @@
+%div{ id: dom_id(@draft_revision, :ineligibilite_rules) }
+ = render Procedure::PendingRepublishComponent.new(procedure: @draft_revision.procedure, render_if: pending_changes?)
+ = render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs)
+ .fr-fieldset
+ = form_for(@draft_revision, url: change_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), html: { id: 'ineligibilite_form', class: 'width-100' }) do |f|
+ .fr-fieldset__element
+ .fr-toggle.fr-toggle--label-left
+ = f.check_box :ineligibilite_enabled, class: 'fr-toggle__input', data: @opt
+ = f.label :ineligibilite_enabled, "Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité", data: { 'fr-checked-label': "Activé", 'fr-unchecked-label': "Désactivé" }, class: 'fr-toggle__label'
+
+ .fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :ineligibilite_message, input_type: :text_area, opts: {rows: 5})
+
+ .fr-mx-1w.fr-label.fr-py-0.fr-mb-1w.fr-mt-2w
+ Conditions d’inéligibilité
+ %span.fr-hint-text Vous pouvez utiliser une ou plusieurs condtions pour bloquer le dépot.
+ .fr-fieldset__element
+ = form_tag admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), method: :patch, data: { turbo: true, controller: 'autosave' }, class: 'form width-100' do
+ .conditionnel.width-100
+ %table.condition-table
+ - if rows.size > 0
+ %thead
+ %tr
+ %th.fr-pt-0.far-left
+ %th.fr-pt-0.target Champ Cible
+ %th.fr-pt-0.operator Opérateur
+ %th.fr-pt-0.value Valeur
+ %th.fr-pt-0.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, operator_name)
+ %td.delete-column= delete_condition_tag(row_index)
+ %tfoot
+ %tr
+ %td.text-right{ colspan: 5 }= add_condition_tag
+
+ .padded-fixed-footer
+ .fixed-footer
+ .fr-container
+ .fr-grid-row.fr-col-offset-md-2.fr-col-md-8
+ .fr-col-12
+ %ul.fr-btns-group.fr-btns-group--inline-md
+ %li
+ = link_to "Annuler et revenir à l'écran de gestion", admin_procedure_path(id: @draft_revision.procedure), class: 'fr-btn fr-btn--secondary', data: { confirm: 'Si vous avez fait des modifications elles ne seront pas sauvegardées.'}
+ %li
+ = button_tag "Enregistrer", class: "fr-btn", form: 'ineligibilite_form'
diff --git a/app/components/dossiers/edit_footer_component.rb b/app/components/dossiers/edit_footer_component.rb
index fca7fab45..5f5bb8980 100644
--- a/app/components/dossiers/edit_footer_component.rb
+++ b/app/components/dossiers/edit_footer_component.rb
@@ -1,4 +1,6 @@
class Dossiers::EditFooterComponent < ApplicationComponent
+ delegate :can_passer_en_construction?, to: :@dossier
+
def initialize(dossier:, annotation:)
@dossier = dossier
@annotation = annotation
@@ -14,20 +16,29 @@ class Dossiers::EditFooterComponent < ApplicationComponent
@annotation.present?
end
+ def disabled_submit_buttons_options
+ {
+ class: 'fr-text--sm fr-mb-0 fr-mr-2w',
+ data: { 'fr-opened': "true" },
+ aria: { controls: 'modal-eligibilite-rules-dialog' }
+ }
+ end
+
def submit_draft_button_options
{
class: 'fr-btn fr-btn--sm',
- disabled: !owner?,
+ disabled: !owner? || !can_passer_en_construction?,
method: :post,
- data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' }
+ data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server }
}
end
def submit_en_construction_button_options
{
class: 'fr-btn fr-btn--sm',
+ disabled: !can_passer_en_construction?,
method: :post,
- data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' },
+ data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server },
form: { id: "form-submit-en-construction" }
}
end
diff --git a/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml b/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml
index 098e6ec0b..b6de7d121 100644
--- a/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml
+++ b/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml
@@ -2,5 +2,6 @@
en:
submit: Submit the file
submit_changes: Submit file changes
+ submit_disabled: File submission disabled
submitting: Submitting…
invite_notice: You are invited to make amendments to this file but only the owner themselves can submit it.
diff --git a/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml b/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml
index 33937aed6..8ffd062db 100644
--- a/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml
+++ b/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml
@@ -2,5 +2,6 @@
fr:
submit: Déposer le dossier
submit_changes: Déposer les modifications
+ submit_disabled: Pourquoi je ne peux pas déposer mon dossier ?
submitting: Envoi en cours…
invite_notice: En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le déposer lui-même.
diff --git a/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml b/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml
index 77540bd16..2f0f59b2b 100644
--- a/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml
+++ b/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml
@@ -3,8 +3,13 @@
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
- if !annotation? && @dossier.can_transition_to_en_construction?
+ - if !can_passer_en_construction?
+ = link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
= button_to t('.submit'), brouillon_dossier_url(@dossier), submit_draft_button_options
- - elsif @dossier.forked_with_changes?
+
+ - if @dossier.forked_with_changes?
+ - if !can_passer_en_construction?
+ = link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
= button_to t('.submit_changes'), modifier_dossier_url(@dossier.editing_fork_origin), submit_en_construction_button_options
diff --git a/app/components/dossiers/errors_full_messages_component.rb b/app/components/dossiers/errors_full_messages_component.rb
index fd8bafd94..207170e8c 100644
--- a/app/components/dossiers/errors_full_messages_component.rb
+++ b/app/components/dossiers/errors_full_messages_component.rb
@@ -3,17 +3,15 @@
class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
ErrorDescriptor = Data.define(:anchor, :label, :error_message)
- def initialize(dossier:, errors:)
+ def initialize(dossier:)
@dossier = dossier
- @errors = errors
end
def dedup_and_partitioned_errors
- formated_errors = @errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
+ @dossier.errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
.to_a # but enum.to_a gives back an array
.uniq { |error| [error.inner_error.base] } # dedup cumulated errors from dossier.champs, dossier.champs_public, dossier.champs_private which run the validator one time per association
.map { |error| to_error_descriptor(error) }
- yield(Array(formated_errors[0..2]), Array(formated_errors[3..]))
end
def to_error_descriptor(error)
@@ -27,6 +25,6 @@ class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
end
def render?
- !@errors.empty?
+ !@dossier.errors.empty?
end
end
diff --git a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml
index 3fab8164d..0a595e80a 100644
--- a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml
+++ b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml
@@ -5,4 +5,3 @@ en:
Your file has 1 error. Fix-it to continue :
other: |
Your file has %{count} errors. Fix-them to continue :
- see_more: Show all errors
diff --git a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml
index 1fd0e7f8c..3d94f636f 100644
--- a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml
+++ b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml
@@ -5,4 +5,3 @@ fr:
Votre dossier contient 1 champ en erreur. Corrigez-la pour poursuivre :
other: |
Votre dossier contient %{count} champs en erreurs. Corrigez-les pour poursuivre :
- see_more: Afficher toutes les erreurs
diff --git a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml
index 58d76cb56..ada4150b5 100644
--- a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml
+++ b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml
@@ -1,15 +1,4 @@
.fr-alert.fr-alert--error.fr-mb-3w{ role: "alertdialog" }
- - dedup_and_partitioned_errors do |head, tail|
- %p#sumup-errors= t('.sumup_html', count: head.size + tail.size, url: head.first.anchor)
- %ul.fr-mb-0#head-errors
- - head.each do |error_descriptor|
- %li
- = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
- = error_descriptor.error_message
- - if tail.size > 0
- %button{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline" }= t('.see_more')
- %ul#tail-errors.fr-collapse.fr-mt-0
- - tail.each do |error_descriptor|
- %li
- = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
- = "(#{error_descriptor.error_message})"
+ - if dedup_and_partitioned_errors.size > 0
+ %p#sumup-errors= t('.sumup_html', count: dedup_and_partitioned_errors.size, url: dedup_and_partitioned_errors.first.anchor)
+ = render ExpandableErrorList.new(errors: dedup_and_partitioned_errors)
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component.rb b/app/components/dossiers/invalid_ineligibilite_rules_component.rb
new file mode 100644
index 000000000..526bdbc94
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component.rb
@@ -0,0 +1,16 @@
+class Dossiers::InvalidIneligibiliteRulesComponent < ApplicationComponent
+ delegate :can_passer_en_construction?, to: :@dossier
+
+ def initialize(dossier:)
+ @dossier = dossier
+ @revision = dossier.revision
+ end
+
+ def render?
+ !can_passer_en_construction?
+ end
+
+ def error_message
+ @dossier.revision.ineligibilite_message
+ end
+end
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml
new file mode 100644
index 000000000..1a377763c
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml
@@ -0,0 +1,6 @@
+fr:
+ modal:
+ title: "Your file does not match submission criteria"
+ close: "Close"
+ close_alt: "Close this modal"
+ body: "The procedure « %{procedure_libelle} » have submission criteria, unfortunately your file does not match them. You can not submit your file"
\ No newline at end of file
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml
new file mode 100644
index 000000000..d191f03d4
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml
@@ -0,0 +1,5 @@
+fr:
+ modal:
+ title: "Vous ne pouvez pas déposer votre dossier"
+ close: "Fermer"
+ close_alt: "Fermer la fenêtre modale"
\ No newline at end of file
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml
new file mode 100644
index 000000000..dd39925cd
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml
@@ -0,0 +1,16 @@
+%div{ id: dom_id(@dossier, :ineligibilite_rules_broken), data: { controller: 'ineligibilite-rules-match', turbo_force: :server } }
+ %button.fr-sr-only{ aria: {controls: 'modal-eligibilite-rules-dialog' }, data: {'fr-opened': "false" } }
+ show modal
+
+ %dialog.fr-modal{ "aria-labelledby" => "fr-modal-title-modal-1", role: "dialog", id: 'modal-eligibilite-rules-dialog', data: { 'ineligibilite-rules-match-target' => 'dialog' } }
+ .fr-container.fr-container--fluid.fr-container-md
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-12.fr-col-md-8.fr-col-lg-6
+ .fr-modal__body
+ .fr-modal__header
+ %button.fr-btn--close.fr-btn{ aria: { controls: 'modal-eligibilite-rules-dialog' }, title: t('.modal.close_alt') }= t('.modal.close')
+ .fr-modal__content
+ %h1#fr-modal-title-modal-1.fr-modal__title
+ %span.fr-icon-arrow-right-line.fr-icon--lg>
+ = t('.modal.title')
+ %p= error_message
diff --git a/app/components/expandable_error_list.rb b/app/components/expandable_error_list.rb
new file mode 100644
index 000000000..43d5c9215
--- /dev/null
+++ b/app/components/expandable_error_list.rb
@@ -0,0 +1,9 @@
+class ExpandableErrorList < ApplicationComponent
+ def initialize(errors:)
+ @errors = errors
+ end
+
+ def splitted_errors
+ yield(Array(@errors[0..2]), Array(@errors[3..]))
+ end
+end
diff --git a/app/components/expandable_error_list/expandable_error_list.html.en.yml b/app/components/expandable_error_list/expandable_error_list.html.en.yml
new file mode 100644
index 000000000..b21ee7d8a
--- /dev/null
+++ b/app/components/expandable_error_list/expandable_error_list.html.en.yml
@@ -0,0 +1,3 @@
+---
+en:
+ see_more: Show all errors
diff --git a/app/components/expandable_error_list/expandable_error_list.html.fr.yml b/app/components/expandable_error_list/expandable_error_list.html.fr.yml
new file mode 100644
index 000000000..755d13886
--- /dev/null
+++ b/app/components/expandable_error_list/expandable_error_list.html.fr.yml
@@ -0,0 +1,3 @@
+---
+fr:
+ see_more: Afficher toutes les erreurs
diff --git a/app/components/expandable_error_list/expandable_error_list.html.haml b/app/components/expandable_error_list/expandable_error_list.html.haml
new file mode 100644
index 000000000..1ab5221e5
--- /dev/null
+++ b/app/components/expandable_error_list/expandable_error_list.html.haml
@@ -0,0 +1,14 @@
+- splitted_errors do |head, tail|
+ %ul#head-errors.fr-mb-0
+ - head.each do |error_descriptor|
+ %li
+ = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
+ = error_descriptor.error_message
+
+ - if tail.size > 0
+ %button.fr-mt-0.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "" }= t('see_more')
+ %ul#tail-errors.fr-collapse.fr-mt-0
+ - tail.each do |error_descriptor|
+ %li
+ = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
+ = error_descriptor.error_message
diff --git a/app/components/procedure/card/ineligibilite_dossier_component.rb b/app/components/procedure/card/ineligibilite_dossier_component.rb
new file mode 100644
index 000000000..b1d371708
--- /dev/null
+++ b/app/components/procedure/card/ineligibilite_dossier_component.rb
@@ -0,0 +1,19 @@
+class Procedure::Card::IneligibiliteDossierComponent < ApplicationComponent
+ def initialize(procedure:)
+ @procedure = procedure
+ end
+
+ def ready?
+ @procedure.draft_revision
+ .conditionable_types_de_champ
+ .present? && @procedure.draft_revision.ineligibilite_enabled
+ end
+
+ def error?
+ !@procedure.draft_revision.validate(:ineligibilite_rules_editor)
+ end
+
+ def completed?
+ @procedure.draft_revision.ineligibilite_enabled
+ end
+end
diff --git a/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.fr.yml b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.fr.yml
new file mode 100644
index 000000000..6e78d7da6
--- /dev/null
+++ b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.fr.yml
@@ -0,0 +1,8 @@
+---
+fr:
+ title: Inéligibilité des dossiers
+ state:
+ pending: Désactivé
+ ready: À configurer
+ completed: Activé
+ subtitle: Gérez vos conditions d’inéligibilité en fonction des champs du formulaire
diff --git a/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.html.haml b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.html.haml
new file mode 100644
index 000000000..aeced88e6
--- /dev/null
+++ b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.html.haml
@@ -0,0 +1,13 @@
+.fr-col-6.fr-col-md-4.fr-col-lg-3
+ = link_to edit_admin_procedure_ineligibilite_rules_path(@procedure), class: 'fr-tile fr-enlarge-link' do
+ .fr-tile__body.flex.column.align-center.justify-between
+ - if !ready?
+ %p.fr-badge.fr-badge= t('.state.pending')
+ - elsif error?
+ %p.fr-badge.fr-badge--error À modifier
+ - else
+ %p.fr-badge.fr-badge--success= t('.state.completed')
+ %div
+ %h3.fr-h6.fr-mt-10v= t('.title')
+ %p.fr-tile-subtitle= t('.subtitle')
+ %p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')
diff --git a/app/components/procedure/errors_summary.rb b/app/components/procedure/errors_summary.rb
index adce8fd64..bf41ab3da 100644
--- a/app/components/procedure/errors_summary.rb
+++ b/app/components/procedure/errors_summary.rb
@@ -1,4 +1,6 @@
class Procedure::ErrorsSummary < ApplicationComponent
+ ErrorDescriptor = Data.define(:anchor, :label, :error_message)
+
def initialize(procedure:, validation_context:)
@procedure = procedure
@validation_context = validation_context
@@ -24,14 +26,14 @@ class Procedure::ErrorsSummary < ApplicationComponent
@procedure.errors.present?
end
- def error_messages
- @procedure.errors.map do |error|
- [error, error_correction_page(error)]
- end
+ def errors
+ @procedure.errors.map { to_error_descriptor(_1) }
end
def error_correction_page(error)
case error.attribute
+ when :ineligibilite_rules
+ edit_admin_procedure_ineligibilite_rules_path(@procedure)
when :draft_types_de_champ_public
tdc = error.options[:type_de_champ]
champs_admin_procedure_path(@procedure, anchor: dom_id(tdc.stable_self, :editor_error))
@@ -45,4 +47,14 @@ class Procedure::ErrorsSummary < ApplicationComponent
edit_admin_procedure_mail_template_path(@procedure, klass.const_get(:SLUG))
end
end
+
+ def to_error_descriptor(error)
+ libelle = case error.attribute
+ when :draft_types_de_champ_public, :draft_types_de_champ_private
+ error.options[:type_de_champ].libelle.truncate(200)
+ else
+ error.base.class.human_attribute_name(error.attribute)
+ end
+ ErrorDescriptor.new(error_correction_page(error), libelle, error.message)
+ end
end
diff --git a/app/components/procedure/errors_summary/errors_summary.html.haml b/app/components/procedure/errors_summary/errors_summary.html.haml
index 72780bcd1..e5042916e 100644
--- a/app/components/procedure/errors_summary/errors_summary.html.haml
+++ b/app/components/procedure/errors_summary/errors_summary.html.haml
@@ -2,8 +2,4 @@
- if invalid?
= render Dsfr::AlertComponent.new(state: :error, title: , extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
- - error_messages.each do |(error, path)|
- %p.mt-2
- = error.full_message
- - if path.present?
- = "(#{link_to 'corriger', path, class: 'fr-link'})"
+ = render ExpandableErrorList.new(errors:)
diff --git a/app/components/procedure/pending_republish_component.rb b/app/components/procedure/pending_republish_component.rb
new file mode 100644
index 000000000..181eb6f5c
--- /dev/null
+++ b/app/components/procedure/pending_republish_component.rb
@@ -0,0 +1,10 @@
+class Procedure::PendingRepublishComponent < ApplicationComponent
+ def initialize(procedure:, render_if:)
+ @procedure = procedure
+ @render_if = render_if
+ end
+
+ def render?
+ @render_if
+ end
+end
diff --git a/app/components/procedure/pending_republish_component/pending_republish_component.fr.yml b/app/components/procedure/pending_republish_component/pending_republish_component.fr.yml
new file mode 100644
index 000000000..eb941cdba
--- /dev/null
+++ b/app/components/procedure/pending_republish_component/pending_republish_component.fr.yml
@@ -0,0 +1,4 @@
+---
+fr:
+ pending_republish_html: |
+ Ces modifications ne seront appliquées qu'à la prochaine publication. Vous pouvez vérifier puis publier les modifications sur l'écran de gestion de la démarche
\ No newline at end of file
diff --git a/app/components/procedure/pending_republish_component/pending_republish_component.html.haml b/app/components/procedure/pending_republish_component/pending_republish_component.html.haml
new file mode 100644
index 000000000..eab7f62fc
--- /dev/null
+++ b/app/components/procedure/pending_republish_component/pending_republish_component.html.haml
@@ -0,0 +1,3 @@
+= render Dsfr::AlertComponent.new(state: :warning) do |c|
+ - c.with_body do
+ = t('.pending_republish_html', href: admin_procedure_path(@procedure.id))
diff --git a/app/components/procedure/revision_changes_component.rb b/app/components/procedure/revision_changes_component.rb
index e266f13e2..af786e6bc 100644
--- a/app/components/procedure/revision_changes_component.rb
+++ b/app/components/procedure/revision_changes_component.rb
@@ -1,9 +1,13 @@
class Procedure::RevisionChangesComponent < ApplicationComponent
- def initialize(changes:, previous_revision:)
- @changes = changes
+ def initialize(new_revision:, previous_revision:)
@previous_revision = previous_revision
- @public_move_changes, @private_move_changes = changes.filter { _1.op == :move }.partition { !_1.private? }
- @delete_champ_warning = !total_dossiers.zero? && !@changes.all?(&:can_rebase?)
+ @new_revision = new_revision
+
+ @tdc_changes = previous_revision.compare_types_de_champ(new_revision)
+ @public_move_changes, @private_move_changes = @tdc_changes.filter { _1.op == :move }.partition { !_1.private? }
+ @delete_champ_warning = !total_dossiers.zero? && !@tdc_changes.all?(&:can_rebase?)
+
+ @ineligibilite_rules_changes = previous_revision.compare_ineligibilite_rules(new_revision)
end
private
diff --git a/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml b/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml
index 10009ce1e..3228c76a8 100644
--- a/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml
+++ b/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml
@@ -80,3 +80,10 @@ fr:
update_expression_reguliere_exemple_text: L’exemple d’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouvel exemple est « %{to} ».
remove_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été supprimé.
update_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouveau message est « %{to} ».
+ ineligibilite_rules:
+ add: La condition d’inéligibilité « %{new_condition} » a été ajoutée.
+ remove: La condition d’inéligibilité « %{previous_condition} » a été supprimée
+ update: La conditon d’inéligibilité « %{previous_condition} » a été changée pour « %{new_condition} »
+ enabled: "L’inéligibilité des dossiers a été activée"
+ disabled: "L’inéligibilité des dossiers a été désactivée"
+ message_updated: "Le message d’inéligibilité a été changé pour « %{ineligibilite_message} »"
\ No newline at end of file
diff --git a/app/components/procedure/revision_changes_component/revision_changes_component.html.haml b/app/components/procedure/revision_changes_component/revision_changes_component.html.haml
index ba19a0dd9..ed7f550c8 100644
--- a/app/components/procedure/revision_changes_component/revision_changes_component.html.haml
+++ b/app/components/procedure/revision_changes_component/revision_changes_component.html.haml
@@ -2,7 +2,7 @@
- list.with_empty do
= t('.no_changes')
- - @changes.each do |change|
+ - @tdc_changes.each do |change|
- prefix = change.private? ? 'private' : 'public'
- case change.op
- when :add
@@ -176,3 +176,7 @@
- list.with_item do
.fr-alert.fr-alert--warning.fr-mt-1v
= t(".invalid_routing_rules_alert")
+
+ - @ineligibilite_rules_changes.each do |change|
+ - list.with_item do
+ = t(".ineligibilite_rules.#{change.op}", **change.i18n_params)
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 60cf82102..d28769213 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
@@ -10,7 +10,7 @@
.flex.justify-start.width-33
.cell.flex.justify-start.column.flex-grow
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ)
- = form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
+ = form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules? || coordinate.used_by_ineligibilite_rules?
.flex.column.justify-start.flex-grow
.cell
@@ -136,6 +136,10 @@
%span
utilisé pour
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
+ - elsif coordinate.used_by_ineligibilite_rules?
+ %span
+ utilisé pour
+ = link_to('l’eligibilité des dossiers', edit_admin_procedure_ineligibilite_rules_path(revision.procedure_id))
- else
= button_to type_de_champ_path, class: 'fr-btn fr-btn--tertiary-no-outline fr-icon-delete-line', title: "Supprimer le champ", method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
%span.sr-only Supprimer
diff --git a/app/controllers/administrateurs/ineligibilite_rules_controller.rb b/app/controllers/administrateurs/ineligibilite_rules_controller.rb
new file mode 100644
index 000000000..41d6865f9
--- /dev/null
+++ b/app/controllers/administrateurs/ineligibilite_rules_controller.rb
@@ -0,0 +1,74 @@
+module Administrateurs
+ class IneligibiliteRulesController < AdministrateurController
+ before_action :retrieve_procedure
+
+ def edit
+ end
+
+ def change
+ if draft_revision.update(procedure_revision_params)
+ redirect_to edit_admin_procedure_ineligibilite_rules_path(@procedure)
+ else
+ flash[:alert] = draft_revision.errors.full_messages
+ render :edit
+ end
+ end
+
+ def add_row
+ condition = Logic.add_empty_condition_to(draft_revision.ineligibilite_rules)
+ draft_revision.update!(ineligibilite_rules: condition)
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ def delete_row
+ condition = condition_form.delete_row(row_index).to_condition
+ draft_revision.update!(ineligibilite_rules: condition)
+
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ def update
+ condition = condition_form.to_condition
+ draft_revision.update!(ineligibilite_rules: condition)
+
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ def change_targeted_champ
+ condition = condition_form.change_champ(row_index).to_condition
+ draft_revision.update!(ineligibilite_rules: condition)
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ private
+
+ def build_ineligibilite_rules_component
+ Conditions::IneligibiliteRulesComponent.new(draft_revision: draft_revision)
+ end
+
+ def draft_revision
+ @procedure.draft_revision
+ end
+
+ def condition_form
+ ConditionForm.new(ineligibilite_rules_params.merge(source_tdcs: draft_revision.types_de_champ_for(scope: :public)))
+ end
+
+ def ineligibilite_rules_params
+ params
+ .require(:procedure_revision)
+ .require(:condition_form)
+ .permit(:top_operator_name, rows: [:targeted_champ, :operator_name, :value])
+ end
+
+ def row_index
+ params[:row_index].to_i
+ end
+
+ def procedure_revision_params
+ params
+ .require(:procedure_revision)
+ .permit(:ineligibilite_message, :ineligibilite_enabled)
+ end
+ end
+end
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index acdfd1332..1fac35eae 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -231,9 +231,9 @@ module Users
def submit_brouillon
@dossier = dossier_with_champs(pj_template: false)
- @errors = submit_dossier_and_compute_errors
+ submit_dossier_and_compute_errors
- if @errors.blank?
+ if @dossier.errors.blank? && @dossier.can_passer_en_construction?
@dossier.passer_en_construction!
@dossier.process_declarative!
@dossier.process_sva_svr!
@@ -278,9 +278,9 @@ module Users
editing_fork_origin.resolve_pending_correction
end
- @errors = submit_dossier_and_compute_errors
+ submit_dossier_and_compute_errors
- if @errors.blank?
+ if @dossier.errors.blank? && @dossier.can_passer_en_construction?
editing_fork_origin.merge_fork(@dossier)
editing_fork_origin.submit_en_construction!
@@ -288,7 +288,6 @@ module Users
else
respond_to do |format|
format.html do
- @dossier = editing_fork_origin
render :modifier
end
@@ -303,10 +302,10 @@ module Users
def update
@dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier
@dossier = dossier_with_champs(pj_template: false)
- @errors = update_dossier_and_compute_errors
-
- @dossier.index_search_terms_later if @errors.empty?
-
+ @can_passer_en_construction_was = @dossier.can_passer_en_construction?
+ update_dossier_and_compute_errors
+ @dossier.index_search_terms_later if @dossier.errors.empty?
+ @can_passer_en_construction_is = @dossier.can_passer_en_construction?
respond_to do |format|
format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?))
@@ -567,21 +566,14 @@ module Users
def submit_dossier_and_compute_errors
@dossier.validate(:champs_public_value)
-
- errors = @dossier.errors
- @dossier.check_mandatory_and_visible_champs.each do |error_on_champ|
- errors.import(error_on_champ)
- end
+ @dossier.check_mandatory_and_visible_champs
if @dossier.editing_fork_origin&.pending_correction?
@dossier.editing_fork_origin.validate(:champs_public_value)
@dossier.editing_fork_origin.errors.where(:pending_correction).each do |error|
- errors.import(error)
+ @dossier.errors.import(error)
end
-
end
-
- errors
end
def ensure_ownership!
diff --git a/app/javascript/controllers/ineligibilite_rules_match_controller.ts b/app/javascript/controllers/ineligibilite_rules_match_controller.ts
new file mode 100644
index 000000000..5b47d79b5
--- /dev/null
+++ b/app/javascript/controllers/ineligibilite_rules_match_controller.ts
@@ -0,0 +1,19 @@
+import { ApplicationController } from './application_controller';
+declare interface modal {
+ disclose: () => void;
+}
+declare interface dsfr {
+ modal: modal;
+}
+declare const window: Window &
+ typeof globalThis & { dsfr: (elem: HTMLElement) => dsfr };
+
+export class InvalidIneligibiliteRulesController extends ApplicationController {
+ static targets = ['dialog'];
+
+ declare dialogTarget: HTMLElement;
+
+ connect() {
+ setTimeout(() => window.dsfr(this.dialogTarget).modal.disclose(), 100);
+ }
+}
diff --git a/app/models/concerns/champ_conditional_concern.rb b/app/models/concerns/champ_conditional_concern.rb
index 9e6559be9..63001229d 100644
--- a/app/models/concerns/champ_conditional_concern.rb
+++ b/app/models/concerns/champ_conditional_concern.rb
@@ -21,6 +21,10 @@ module ChampConditionalConcern
end
end
+ def reset_visible # recompute after a dossier update
+ remove_instance_variable :@visible if instance_variable_defined? :@visible
+ end
+
private
def champs_for_condition
diff --git a/app/models/concerns/dossier_rebase_concern.rb b/app/models/concerns/dossier_rebase_concern.rb
index dd6395dc2..49807793c 100644
--- a/app/models/concerns/dossier_rebase_concern.rb
+++ b/app/models/concerns/dossier_rebase_concern.rb
@@ -22,7 +22,7 @@ module DossierRebaseConcern
end
def pending_changes
- procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
+ procedure.published_revision.present? ? revision.compare_types_de_champ(procedure.published_revision) : []
end
def can_rebase_mandatory_change?(stable_id)
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index e609323bb..343baf06d 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -156,7 +156,7 @@ class Dossier < ApplicationRecord
state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction, after_commit: :after_commit_passer_en_construction do
- transitions from: :brouillon, to: :en_construction
+ transitions from: :brouillon, to: :en_construction, guard: :can_passer_en_construction?
end
event :passer_en_instruction, after: :after_passer_en_instruction, after_commit: :after_commit_passer_en_instruction do
@@ -562,6 +562,12 @@ class Dossier < ApplicationRecord
procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
end
+ def can_passer_en_construction?
+ return true if !revision.ineligibilite_enabled
+
+ !revision.ineligibilite_rules.compute(champs_for_revision(scope: :public))
+ end
+
def can_passer_en_instruction?
return false if blocked_with_pending_correction?
@@ -936,6 +942,7 @@ class Dossier < ApplicationRecord
.map do |champ|
champ.errors.add(:value, :missing)
end
+ .each { errors.import(_1) }
end
def demander_un_avis!(avis)
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 3d6c05b1f..cbdb7a5ed 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -293,7 +293,7 @@ class Procedure < ApplicationRecord
validates_with MonAvisEmbedValidator
- validates_associated :draft_revision, on: :publication
+ validate :validates_associated_draft_revision_with_context
validates_associated :initiated_mail, on: :publication
validates_associated :received_mail, on: :publication
validates_associated :closed_mail, on: :publication
@@ -431,11 +431,15 @@ class Procedure < ApplicationRecord
def draft_changed?
preload_draft_and_published_revisions
- !brouillon? && published_revision.different_from?(draft_revision) && revision_changes.present?
+ !brouillon? && (types_de_champ_revision_changes.present? || ineligibilite_rules_revision_changes.present?)
end
- def revision_changes
- published_revision.compare(draft_revision)
+ def types_de_champ_revision_changes
+ published_revision.compare_types_de_champ(draft_revision)
+ end
+
+ def ineligibilite_rules_revision_changes
+ published_revision.compare_ineligibilite_rules(draft_revision)
end
def preload_draft_and_published_revisions
@@ -1017,6 +1021,13 @@ class Procedure < ApplicationRecord
private
+ def validates_associated_draft_revision_with_context
+ return if draft_revision.blank?
+ return if draft_revision.validate(validation_context)
+
+ draft_revision.errors.map { errors.import(_1) }
+ end
+
def validate_auto_archive_on_in_the_future
return if auto_archive_on.nil?
return if auto_archive_on.future?
diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb
index fef3516d5..0a27fec2c 100644
--- a/app/models/procedure_revision.rb
+++ b/app/models/procedure_revision.rb
@@ -1,4 +1,5 @@
class ProcedureRevision < ApplicationRecord
+ include Logic
self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
@@ -17,8 +18,19 @@ class ProcedureRevision < ApplicationRecord
scope :ordered, -> { order(:created_at) }
+ validates :ineligibilite_message, presence: true, if: -> { ineligibilite_enabled? }
+
delegate :path, to: :procedure, prefix: true
+ validate :ineligibilite_rules_are_valid?,
+ on: [:ineligibilite_rules_editor, :publication]
+ validates :ineligibilite_message,
+ presence: true,
+ if: -> { ineligibilite_enabled? },
+ on: [:ineligibilite_rules_editor, :publication]
+
+ serialize :ineligibilite_rules, LogicSerializer
+
def build_champs_public
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
types_de_champ_public.reload.map(&:build_champ)
@@ -136,16 +148,18 @@ class ProcedureRevision < ApplicationRecord
!draft?
end
- def different_from?(revision)
- revision_types_de_champ != revision.revision_types_de_champ
- end
-
- def compare(revision)
+ def compare_types_de_champ(revision)
changes = []
changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ)
changes
end
+ def compare_ineligibilite_rules(revision)
+ changes = []
+ changes += compare_revision_ineligibilite_rules(revision)
+ changes
+ end
+
def dossier_for_preview(user)
dossier = Dossier
.create_with(autorisation_donnees: true)
@@ -251,6 +265,10 @@ class ProcedureRevision < ApplicationRecord
types_de_champ_public.filter(&:routable?)
end
+ def conditionable_types_de_champ
+ types_de_champ_for(scope: :public).filter(&:conditionable?)
+ end
+
private
def compute_estimated_fill_duration
@@ -318,6 +336,29 @@ class ProcedureRevision < ApplicationRecord
end
end
+ def compare_revision_ineligibilite_rules(new_revision)
+ from_ineligibilite_rules = ineligibilite_rules
+ to_ineligibilite_rules = new_revision.ineligibilite_rules
+ changes = []
+
+ if from_ineligibilite_rules.present? && to_ineligibilite_rules.blank?
+ changes << ProcedureRevisionChange::RemoveEligibiliteRuleChange
+ end
+ if from_ineligibilite_rules.blank? && to_ineligibilite_rules.present?
+ changes << ProcedureRevisionChange::AddEligibiliteRuleChange
+ end
+ if from_ineligibilite_rules != to_ineligibilite_rules
+ changes << ProcedureRevisionChange::UpdateEligibiliteRuleChange
+ end
+ if ineligibilite_message != new_revision.ineligibilite_message
+ changes << ProcedureRevisionChange::UpdateEligibiliteMessageChange
+ end
+ if ineligibilite_enabled != new_revision.ineligibilite_enabled
+ changes << (new_revision.ineligibilite_enabled ? ProcedureRevisionChange::EligibiliteEnabledChange : ProcedureRevisionChange::EligibiliteDisabledChange)
+ end
+ changes.map { _1.new(self, new_revision) }
+ end
+
def compare_type_de_champ(from_type_de_champ, to_type_de_champ, from_coordinates, to_coordinates)
changes = []
if from_type_de_champ.type_champ != to_type_de_champ.type_champ
@@ -442,6 +483,13 @@ class ProcedureRevision < ApplicationRecord
changes
end
+ def ineligibilite_rules_are_valid?
+ if ineligibilite_rules
+ ineligibilite_rules.errors(types_de_champ_for(scope: :public).to_a)
+ .each { errors.add(:ineligibilite_rules, :invalid) }
+ end
+ end
+
def replace_type_de_champ_by_clone(coordinate)
cloned_type_de_champ = coordinate.type_de_champ.deep_clone do |original, kopy|
ClonePiecesJustificativesService.clone_attachments(original, kopy)
diff --git a/app/models/procedure_revision_change.rb b/app/models/procedure_revision_change.rb
index fc412cc26..7d99f0fd2 100644
--- a/app/models/procedure_revision_change.rb
+++ b/app/models/procedure_revision_change.rb
@@ -1,17 +1,19 @@
class ProcedureRevisionChange
- attr_reader :type_de_champ
- def initialize(type_de_champ)
- @type_de_champ = type_de_champ
+ class TypeDeChange
+ attr_reader :type_de_champ
+ def initialize(type_de_champ)
+ @type_de_champ = type_de_champ
+ end
+
+ def label = @type_de_champ.libelle
+ def stable_id = @type_de_champ.stable_id
+ def private? = @type_de_champ.private?
+ def child? = @type_de_champ.child?
+
+ def to_h = { op:, stable_id:, label:, private: private? }
end
- def label = @type_de_champ.libelle
- def stable_id = @type_de_champ.stable_id
- def private? = @type_de_champ.private?
- def child? = @type_de_champ.child?
-
- def to_h = { op:, stable_id:, label:, private: private? }
-
- class AddChamp < ProcedureRevisionChange
+ class AddChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@@ -23,7 +25,7 @@ class ProcedureRevisionChange
def to_h = super.merge(mandatory: mandatory?)
end
- class RemoveChamp < ProcedureRevisionChange
+ class RemoveChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@@ -32,7 +34,7 @@ class ProcedureRevisionChange
def can_rebase?(dossier = nil) = true
end
- class MoveChamp < ProcedureRevisionChange
+ class MoveChamp < TypeDeChange
attr_reader :from, :to
def initialize(type_de_champ, from, to)
@@ -46,7 +48,7 @@ class ProcedureRevisionChange
def to_h = super.merge(from:, to:)
end
- class UpdateChamp < ProcedureRevisionChange
+ class UpdateChamp < TypeDeChange
attr_reader :attribute, :from, :to
def initialize(type_de_champ, attribute, from, to)
@@ -75,4 +77,48 @@ class ProcedureRevisionChange
end
end
end
+
+ class EligibiliteRulesChange
+ attr_reader :previous_revision, :new_revision
+ def initialize(previous_revision, new_revision)
+ @previous_revision = previous_revision
+ @new_revision = new_revision
+ @previous_ineligibilite_rules = @previous_revision.ineligibilite_rules
+ @new_ineligibilite_rules = @new_revision.ineligibilite_rules
+ end
+
+ def i18n_params
+ {
+ previous_condition: @previous_ineligibilite_rules&.to_s(previous_revision.types_de_champ.filter { @previous_ineligibilite_rules.sources.include? _1.stable_id }),
+ new_condition: @new_ineligibilite_rules&.to_s(new_revision.types_de_champ.filter { @new_ineligibilite_rules.sources.include? _1.stable_id })
+ }
+ end
+ end
+
+ class AddEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :add
+ end
+
+ class RemoveEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :remove
+ end
+
+ class UpdateEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :update
+ end
+
+ class EligibiliteEnabledChange < EligibiliteRulesChange
+ def op = :enabled
+ def i18n_params = {}
+ end
+
+ class EligibiliteDisabledChange < EligibiliteRulesChange
+ def op = :disabled
+ def i18n_params = {}
+ end
+
+ class UpdateEligibiliteMessageChange < EligibiliteRulesChange
+ def op = :message_updated
+ def i18n_params = { ineligibilite_message: @new_revision.ineligibilite_message }
+ end
end
diff --git a/app/models/procedure_revision_type_de_champ.rb b/app/models/procedure_revision_type_de_champ.rb
index c4842da20..506e205f7 100644
--- a/app/models/procedure_revision_type_de_champ.rb
+++ b/app/models/procedure_revision_type_de_champ.rb
@@ -75,4 +75,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
def used_by_routing_rules?
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
end
+
+ def used_by_ineligibilite_rules?
+ revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
+ end
end
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index 10e415c05..cf2c321d8 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -657,6 +657,10 @@ class TypeDeChamp < ApplicationRecord
type_champ.in?(ROUTABLE_TYPES)
end
+ def conditionable?
+ Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(type_champ)
+ end
+
def invalid_regexp?
self.errors.delete(:expression_reguliere)
self.errors.delete(:expression_reguliere_exemple_text)
diff --git a/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml
new file mode 100644
index 000000000..a0ace0eca
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml
@@ -0,0 +1,7 @@
+- rendered = render @ineligibilite_rules_component
+
+- if rendered.present?
+ = turbo_stream.replace dom_id(@procedure.draft_revision, :ineligibilite_rules) do
+ - rendered
+- else
+ = turbo_stream.remove dom_id(@procedure.draft_revision, :ineligibilite_rules)
diff --git a/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/edit.html.haml b/app/views/administrateurs/ineligibilite_rules/edit.html.haml
new file mode 100644
index 000000000..6eb91d12f
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/edit.html.haml
@@ -0,0 +1,28 @@
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Démarches', admin_procedures_path],
+ [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['Inéligibilité des dossiers']] }
+
+
+.fr-container
+ .fr-grid-row
+ .fr-col-12.fr-col-offset-md-2.fr-col-md-8
+ %h1.fr-h1 Inéligibilité des dossiers
+
+ = render Dsfr::AlertComponent.new(title: nil, size: :sm, state: :info, heading_level: 'h2', extra_class_names: 'fr-my-2w') do |c|
+ - c.with_body do
+ %p
+ Les dossiers répondant à vos conditions d’inéligibilité ne pourront pas être déposés. Plus d’informations sur l’inéligibilité des dossiers dans la
+ = link_to('doc', ELIGIBILITE_URL, title: "Document sur l’inéligibilité des dossiers", **external_link_attributes)
+
+ - if !@procedure.draft_revision.conditionable_types_de_champ.present?
+ %p.fr-mt-2w.fr-mb-2w
+ Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire :
+ %ul
+ - Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.each do
+ %li= "« #{t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »"
+ %p.fr-mt-2w
+ = link_to 'Ajouter un champ supportant les conditions d’inéligibilité', champs_admin_procedure_path(@procedure), class: 'fr-link fr-icon-arrow-right-line fr-link--icon-right'
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure)
+ - else
+ = render Conditions::IneligibiliteRulesComponent.new(draft_revision: @procedure.draft_revision)
diff --git a/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/procedures/_publication_form.html.haml b/app/views/administrateurs/procedures/_publication_form.html.haml
index 0c9cc8454..d8d96870e 100644
--- a/app/views/administrateurs/procedures/_publication_form.html.haml
+++ b/app/views/administrateurs/procedures/_publication_form.html.haml
@@ -8,7 +8,7 @@
%p.mb-2= t('.draft_changed_procedure_alert')
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
- = render Procedure::RevisionChangesComponent.new changes: procedure.revision_changes, previous_revision: procedure.published_revision
+ = render Procedure::RevisionChangesComponent.new new_revision: procedure.draft_revision, previous_revision: procedure.published_revision
- if procedure.close?
= render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f }
- elsif @procedure.brouillon? && @procedure.missing_steps.empty?
diff --git a/app/views/administrateurs/procedures/modifications.html.haml b/app/views/administrateurs/procedures/modifications.html.haml
index 978fee30f..73b8673bd 100644
--- a/app/views/administrateurs/procedures/modifications.html.haml
+++ b/app/views/administrateurs/procedures/modifications.html.haml
@@ -13,7 +13,6 @@
- previous_revision = nil
- @procedure.revisions.each do |revision|
- if previous_revision.present? && !revision.draft?
- - changes = previous_revision.compare(revision)
- dossiers = revision.dossiers.visible_by_administration
- dossiers_en_construction_count = dossiers.state_en_construction.count
- dossiers_en_instruction_count = dossiers.state_en_instruction.count
@@ -31,7 +30,7 @@
%p= t('.dossiers_en_construction', count: dossiers_en_construction_count)
- elsif !dossiers_en_instruction_count.zero?
%p= t('.dossiers_en_instruction', count: dossiers_en_instruction_count)
- = render Procedure::RevisionChangesComponent.new changes:, previous_revision:
+ = render Procedure::RevisionChangesComponent.new new_revision: revision, previous_revision:
- previous_revision = revision
= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml
index 345eb2824..4463a86cf 100644
--- a/app/views/administrateurs/procedures/show.html.haml
+++ b/app/views/administrateurs/procedures/show.html.haml
@@ -30,8 +30,8 @@
- if @procedure.draft_changed?
= render Dsfr::CalloutComponent.new(title: t(:has_changes, scope: [:administrateurs, :revision_changes]), icon: "fr-fi-information-line") do |c|
- c.with_body do
- = render Procedure::RevisionChangesComponent.new changes: @procedure.revision_changes, previous_revision: @procedure.published_revision
= render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
+ = render Procedure::RevisionChangesComponent.new new_revision: @procedure.draft_revision, previous_revision: @procedure.published_revision
- c.with_bottom do
%ul.fr-mt-2w.fr-btns-group.fr-btns-group--inline
@@ -71,6 +71,7 @@
= render Procedure::Card::PresentationComponent.new(procedure: @procedure)
= render Procedure::Card::ZonesComponent.new(procedure: @procedure) if Rails.application.config.ds_zonage_enabled
= render Procedure::Card::ChampsComponent.new(procedure: @procedure)
+ = render Procedure::Card::IneligibiliteDossierComponent.new(procedure: @procedure)
= render Procedure::Card::ServiceComponent.new(procedure: @procedure, administrateur: current_administrateur)
= render Procedure::Card::AdministrateursComponent.new(procedure: @procedure)
= render Procedure::Card::InstructeursComponent.new(procedure: @procedure)
diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml
index d5fff3262..c797d35f2 100644
--- a/app/views/shared/dossiers/_edit.html.haml
+++ b/app/views/shared/dossiers/_edit.html.haml
@@ -10,7 +10,7 @@
= render NestedForms::FormOwnerComponent.new
= form_for dossier_for_editing, url: brouillon_dossier_url(dossier), method: :patch, html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } do |f|
- = render Dossiers::ErrorsFullMessagesComponent.new(dossier: @dossier, errors: @errors || [])
+ = render Dossiers::ErrorsFullMessagesComponent.new(dossier: dossier)
%header.mb-6
.fr-highlight
%p.fr-text--sm
@@ -25,4 +25,6 @@
= render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier)
+ = render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier)
+
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)
diff --git a/app/views/users/dossiers/update.turbo_stream.haml b/app/views/users/dossiers/update.turbo_stream.haml
index 91a898ab0..8224c1abd 100644
--- a/app/views/users/dossiers/update.turbo_stream.haml
+++ b/app/views/users/dossiers/update.turbo_stream.haml
@@ -1 +1,7 @@
= render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier }
+
+- if !params.key?(:validate)
+ - if @can_passer_en_construction_was && !@can_passer_en_construction_is
+ = turbo_stream.append('contenu', render(Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: @dossier)))
+ - else @ineligibilite_rules_is_computable
+ = turbo_stream.remove(dom_id(@dossier, :ineligibilite_rules_broken))
diff --git a/config/env.example.optional b/config/env.example.optional
index b9cc395e9..79710f04e 100644
--- a/config/env.example.optional
+++ b/config/env.example.optional
@@ -61,6 +61,9 @@ DS_ENV="staging"
# Instance customization: URL of the Routage documentation
# ROUTAGE_URL=""
#
+# Instance customization: URL of the EligibiliteDossier documentation
+# ELIGIBILITE_URL=""
+#
# Instance customization: URL of the accessibility statement
# ACCESSIBILITE_URL=""
diff --git a/config/initializers/02_urls.rb b/config/initializers/02_urls.rb
index d6e031dae..c29ea1d5d 100644
--- a/config/initializers/02_urls.rb
+++ b/config/initializers/02_urls.rb
@@ -37,6 +37,7 @@ CGU_URL = ENV.fetch("CGU_URL", [DOC_URL, "cgu"].join("/"))
MENTIONS_LEGALES_URL = ENV.fetch("MENTIONS_LEGALES_URL", "/mentions-legales")
ACCESSIBILITE_URL = ENV.fetch("ACCESSIBILITE_URL", "/declaration-accessibilite")
ROUTAGE_URL = ENV.fetch("ROUTAGE_URL", [DOC_URL, "/pour-aller-plus-loin/routage"].join("/"))
+ELIGIBILITE_URL = ENV.fetch("ELIGIBILITE_URL", [DOC_URL, "/pour-aller-plus-loin/eligibilite-des-dossiers"].join("/"))
API_DOC_URL = [DOC_URL, "api-graphql"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
WEBHOOK_ALTERNATIVE_DOC_URL = [DOC_URL, "api-graphql", "cas-dusages-exemple-dimplementation", "synchroniser-les-dossiers-modifies-sur-ma-demarche"].join("/")
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 17b7328d0..a05c394b2 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -606,6 +606,7 @@ en:
otp_attempt: 'OTP code (only if you have already activated 2FA)'
procedure:
zone: This procedure is run by
+ ineligibilite_rules: "Eligibility rules"
champs:
value: Value
default_mail_attributes: &default_mail_attributes
@@ -667,6 +668,10 @@ en:
path:
taken: is already used for procedure. You cannot use it because it belongs to another administrator.
invalid: is not valid. It must countain between 3 and 200 characters among a-z, 0-9, '_' and '-'.
+ procedure_revision:
+ attributes:
+ ineligibilite_rules:
+ invalid: are invalid
"dossier/champs":
format: "%{message}"
attributes:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 227f89a6e..42896bd24 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -610,6 +610,7 @@ fr:
otp_attempt: 'Code OTP (uniquement si vous avez déjà activé 2FA)'
procedure:
zone: La démarche est mise en œuvre par
+ ineligibilite_rules: "Les règles d’inéligibilité"
champs:
value: Valeur du champ
default_mail_attributes: &default_mail_attributes
@@ -669,6 +670,10 @@ fr:
path:
taken: est déjà utilisé par une démarche. Vous ne pouvez pas l’utiliser car il appartient à un autre administrateur.
invalid: n’est pas valide. Il doit comporter au moins 3 caractères, au plus 200 caractères et seuls les caractères a-z, 0-9, '_' et '-' sont autorisés.
+ procedure_revision:
+ attributes:
+ ineligibilite_rules:
+ invalid: ne sont pas valides
"dossier/champs":
format: "%{message}"
attributes:
diff --git a/config/locales/models/procedure/en.yml b/config/locales/models/procedure/en.yml
index 55a9c1ddd..34fc89e35 100644
--- a/config/locales/models/procedure/en.yml
+++ b/config/locales/models/procedure/en.yml
@@ -72,16 +72,16 @@ en:
invalid: 'invalid format'
draft_types_de_champ_public:
format: 'Public field %{message}'
- invalid_condition: "« %{value} » have an invalid logic"
- empty_repetition: '« %{value} » requires at least one field'
- empty_drop_down: '« %{value} » requires at least one option'
- inconsistent_header_section: "« %{value} » %{custom_message}"
+ invalid_condition: "have an invalid logic"
+ empty_repetition: 'requires at least one field'
+ empty_drop_down: 'requires at least one option'
+ inconsistent_header_section: "%{custom_message}"
draft_types_de_champ_private:
format: 'Private field %{message}'
- invalid_condition: "« %{value} » have an invalid logic"
- empty_repetition: '« %{value} » requires at least one field'
- empty_drop_down: '« %{value} » requires at least one option'
- inconsistent_header_section: "« %{value} » %{custom_message}"
+ invalid_condition: "have an invalid logic"
+ empty_repetition: 'requires at least one field'
+ empty_drop_down: 'requires at least one option'
+ inconsistent_header_section: "%{custom_message}"
attestation_template:
format: "%{attribute} %{message}"
initiated_mail:
diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml
index 4a0fdeca5..5a9dfd9ab 100644
--- a/config/locales/models/procedure/fr.yml
+++ b/config/locales/models/procedure/fr.yml
@@ -8,7 +8,7 @@ fr:
procedure:
hints:
description: Décrivez en quelques lignes le contexte, la finalité, etc.
- description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les critères d’éligibilité s’il y en a, les pré-requis, etc.
+ description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les conditions d’éligibilité s’il y en a, les pré-requis, etc.
description_pj: Décrivez la liste des pièces jointes à fournir s’il y en a
lien_site_web: "Il s'agit de la page de votre site web où le lien sera diffusé. Ex: https://exemple.gouv.fr/page_informant_sur_ma_demarche"
cadre_juridique: "Exemple: 'https://www.legifrance.gouv.fr/'"
@@ -78,16 +78,16 @@ fr:
invalid: 'n’a pas le bon format'
draft_types_de_champ_public:
format: 'Le champ %{message}'
- invalid_condition: "« %{value} » a une logique conditionnelle invalide"
- empty_repetition: '« %{value} » doit comporter au moins un champ répétable'
- empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable'
- inconsistent_header_section: "« %{value} » %{custom_message}"
+ invalid_condition: "a une logique conditionnelle invalide"
+ empty_repetition: 'doit comporter au moins un champ répétable'
+ empty_drop_down: 'doit comporter au moins un choix sélectionnable'
+ inconsistent_header_section: "%{custom_message}"
draft_types_de_champ_private:
format: 'L’annotation privée %{message}'
- invalid_condition: "« %{value} » a une logique conditionnelle invalide"
- empty_repetition: '« %{value} » doit comporter au moins un champ répétable'
- empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable'
- inconsistent_header_section: "« %{value} » %{custom_message}"
+ invalid_condition: "a une logique conditionnelle invalide"
+ empty_repetition: 'doit comporter au moins un champ répétable'
+ empty_drop_down: 'doit comporter au moins un choix sélectionnable'
+ inconsistent_header_section: "%{custom_message}"
attestation_template:
format: "%{attribute} %{message}"
initiated_mail:
diff --git a/config/locales/models/procedure_revision/fr.yml b/config/locales/models/procedure_revision/fr.yml
new file mode 100644
index 000000000..1665415aa
--- /dev/null
+++ b/config/locales/models/procedure_revision/fr.yml
@@ -0,0 +1,7 @@
+fr:
+ activerecord:
+ attributes:
+ procedure_revision:
+ ineligibilite_message: Message d’inéligibilité
+ hints:
+ ineligibilite_message: "Ce message sera affiché à l’usager si son dossier est bloqué et lui expliquera la raison de son inéligibilité."
diff --git a/config/routes.rb b/config/routes.rb
index 8b5f2152e..d16b5f778 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -608,6 +608,14 @@ Rails.application.routes.draw do
delete :delete_row, on: :member
end
+ resource :ineligibilite_rules, only: [:edit, :update, :destroy], param: :revision_id do
+ patch :change_targeted_champ, on: :member
+ patch :update_all_rows, on: :member
+ patch :add_row, on: :member
+ delete :delete_row, on: :member
+ patch :change
+ end
+
patch :update_defaut_groupe_instructeur, controller: 'routing_rules', as: :update_defaut_groupe_instructeur
put 'clone'
diff --git a/db/migrate/20240409075536_add_transitions_rules_to_procedure_revisions.rb b/db/migrate/20240409075536_add_transitions_rules_to_procedure_revisions.rb
new file mode 100644
index 000000000..e2a654783
--- /dev/null
+++ b/db/migrate/20240409075536_add_transitions_rules_to_procedure_revisions.rb
@@ -0,0 +1,5 @@
+class AddTransitionsRulesToProcedureRevisions < ActiveRecord::Migration[7.0]
+ def change
+ add_column :procedure_revisions, :ineligibilite_rules, :jsonb
+ end
+end
diff --git a/db/migrate/20240514075727_add_dossier_ineligble_message_to_procedure_revisions.rb b/db/migrate/20240514075727_add_dossier_ineligble_message_to_procedure_revisions.rb
new file mode 100644
index 000000000..bf8464f8c
--- /dev/null
+++ b/db/migrate/20240514075727_add_dossier_ineligble_message_to_procedure_revisions.rb
@@ -0,0 +1,5 @@
+class AddDossierIneligbleMessageToProcedureRevisions < ActiveRecord::Migration[7.0]
+ def change
+ add_column :procedure_revisions, :ineligibilite_message, :text
+ end
+end
diff --git a/db/migrate/20240516095601_add_eligibilite_dossiers_enabled_to_procedure_revisions.rb b/db/migrate/20240516095601_add_eligibilite_dossiers_enabled_to_procedure_revisions.rb
new file mode 100644
index 000000000..19ce1d243
--- /dev/null
+++ b/db/migrate/20240516095601_add_eligibilite_dossiers_enabled_to_procedure_revisions.rb
@@ -0,0 +1,5 @@
+class AddEligibiliteDossiersEnabledToProcedureRevisions < ActiveRecord::Migration[7.0]
+ def change
+ add_column :procedure_revisions, :ineligibilite_enabled, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6f7fcfe7c..b33a10582 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -863,6 +863,9 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_27_090508) do
create_table "procedure_revisions", force: :cascade do |t|
t.datetime "created_at", precision: nil, null: false
t.bigint "dossier_submitted_message_id"
+ t.boolean "ineligibilite_enabled", default: false, null: false
+ t.text "ineligibilite_message"
+ t.jsonb "ineligibilite_rules"
t.bigint "procedure_id", null: false
t.datetime "published_at", precision: nil
t.datetime "updated_at", precision: nil, null: false
diff --git a/spec/components/conditions/ineligibilite_rules_component_spec.rb b/spec/components/conditions/ineligibilite_rules_component_spec.rb
new file mode 100644
index 000000000..c678c5ace
--- /dev/null
+++ b/spec/components/conditions/ineligibilite_rules_component_spec.rb
@@ -0,0 +1,64 @@
+describe Conditions::IneligibiliteRulesComponent, type: :component do
+ include Logic
+ let(:procedure) { create(:procedure) }
+ let(:component) { described_class.new(draft_revision: procedure.draft_revision) }
+
+ describe 'render' do
+ let(:ineligibilite_message) { 'ok' }
+ let(:ineligibilite_enabled) { true }
+ before do
+ procedure.draft_revision.update(ineligibilite_rules:, ineligibilite_message:, ineligibilite_enabled:)
+ end
+ context 'when ineligibilite_rules are valid' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(true)) }
+ it 'does not render error' do
+ render_inline(component)
+ expect(page).not_to have_selector('.errors-summary')
+ end
+ end
+ context 'when ineligibilite_rules are invalid' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
+ it 'does not render error' do
+ render_inline(component)
+ expect(page).to have_selector('.errors-summary')
+ end
+ end
+ end
+
+ describe '#pending_changes' do
+ context 'when procedure is published' do
+ it 'detect changes when setup changes' do
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.draft_revision.ineligibilite_message = 'changed'
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_enabled = true
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_rules = {}
+ expect(component.pending_changes?).to be_falsey
+ end
+ end
+
+ context 'when procedure is published' do
+ let(:procedure) { create(:procedure, :published) }
+ it 'detect changes when setup changes' do
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.draft_revision.ineligibilite_message = 'changed'
+ expect(component.pending_changes?).to be_truthy
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_enabled = true
+ expect(component.pending_changes?).to be_truthy
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_rules = {}
+ expect(component.pending_changes?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/components/dossiers/edit_footer_component_spec.rb b/spec/components/dossiers/edit_footer_component_spec.rb
new file mode 100644
index 000000000..4b8e1a77f
--- /dev/null
+++ b/spec/components/dossiers/edit_footer_component_spec.rb
@@ -0,0 +1,50 @@
+RSpec.describe Dossiers::EditFooterComponent, type: :component do
+ let(:annotation) { false }
+ let(:component) { Dossiers::EditFooterComponent.new(dossier:, annotation:) }
+
+ subject { render_inline(component).to_html }
+
+ before { allow(component).to receive(:owner?).and_return(true) }
+
+ context 'when brouillon' do
+ let(:dossier) { create(:dossier, :brouillon) }
+
+ context 'when dossier can be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(true) }
+ it 'renders submit button without disabled' do
+ expect(subject).to have_selector('button', text: 'Déposer le dossier')
+ end
+ end
+
+ context 'when dossier can not be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(false) }
+ it 'renders submit button with disabled' do
+ expect(subject).to have_selector('a', text: 'Pourquoi je ne peux pas déposer mon dossier ?')
+ expect(subject).to have_selector('button[disabled]', text: 'Déposer le dossier')
+ end
+ end
+ end
+
+ context 'when en construction' do
+ let(:fork_origin) { create(:dossier, :en_construction) }
+ let(:dossier) { fork_origin.clone(fork: true) }
+ before { allow(dossier).to receive(:forked_with_changes?).and_return(true) }
+
+ context 'when dossier can be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(true) }
+
+ it 'renders submit button without disabled' do
+ expect(subject).to have_selector('button', text: 'Déposer les modifications')
+ end
+ end
+
+ context 'when dossier can not be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(false) }
+
+ it 'renders submit button with disabled' do
+ expect(subject).to have_selector('a', text: 'Pourquoi je ne peux pas déposer mon dossier ?')
+ expect(subject).to have_selector('button[disabled]', text: 'Déposer les modifications')
+ end
+ end
+ end
+end
diff --git a/spec/components/procedures/card/ineligibilite_dossier_component.rb b/spec/components/procedures/card/ineligibilite_dossier_component.rb
new file mode 100644
index 000000000..433b59155
--- /dev/null
+++ b/spec/components/procedures/card/ineligibilite_dossier_component.rb
@@ -0,0 +1,25 @@
+describe Procedure::Card::IneligibiliteDossierComponent, type: :component do
+ describe 'render' do
+ subject do
+ render_inline(described_class.new(procedure: procedure))
+ end
+
+ context 'when none of types_de_champ_public supports conditional' do
+ let(:procedure) { create(:procedure, types_de_champ_public: []) }
+
+ it 'render missing setup' do
+ subject
+ expect(page).to have_text('Champs manquant')
+ end
+ end
+
+ context 'when at least one of types_de_champ_public support conditional' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) }
+
+ it 'render the template' do
+ subject
+ expect(page).to have_text('À configurer')
+ end
+ end
+ end
+end
diff --git a/spec/components/procedures/errors_summary_spec.rb b/spec/components/procedures/errors_summary_spec.rb
index 4c3ef6337..ebeb1096d 100644
--- a/spec/components/procedures/errors_summary_spec.rb
+++ b/spec/components/procedures/errors_summary_spec.rb
@@ -11,27 +11,33 @@ describe Procedure::ErrorsSummary, type: :component do
context 'when :publication' do
let(:validation_context) { :publication }
- it 'shows errors for public and private tdc' do
- expect(page).to have_text("Le champ « public » doit comporter au moins un choix sélectionnable")
- expect(page).to have_text("L’annotation privée « private » doit comporter au moins un choix sélectionnable")
+ it 'shows errors and links for public and private tdc' do
+ 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: "private")
+ expect(page).to have_text("doit comporter au moins un choix sélectionnable", count: 2)
end
end
context 'when :types_de_champ_public_editor' do
let(:validation_context) { :types_de_champ_public_editor }
- it 'shows errors for public only tdc' do
- expect(page).to have_text("Le champ « public » doit comporter au moins un choix sélectionnable")
- expect(page).not_to have_text("L’annotation privée « private » doit comporter au moins un choix sélectionnable")
+ 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_selector("a", text: "public")
+ expect(page).to have_text("doit comporter au moins un choix sélectionnable", count: 1)
+ expect(page).not_to have_selector("a", text: "private")
end
end
context 'when :types_de_champ_private_editor' do
let(:validation_context) { :types_de_champ_private_editor }
- it 'shows errors for private only tdc' do
- expect(page).not_to have_text("Le champ « public » doit comporter au moins un choix sélectionnable")
- expect(page).to have_text("L’annotation privée « private » doit comporter au moins un choix sélectionnable")
+ 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_selector("a", text: "private")
+ expect(page).to have_text("doit comporter au moins un choix sélectionnable")
+ expect(page).not_to have_selector("a", text: "public")
end
end
end
@@ -52,16 +58,24 @@ describe Procedure::ErrorsSummary, type: :component do
before { subject }
- it 'renders all errors on champ' do
- expect(page).to have_text("Le champ « drop down list requires options » doit comporter au moins un choix sélectionnable")
- expect(page).to have_text("Le champ « repetition requires children » doit comporter au moins un champ répétable")
- expect(page).to have_text("Le champ « invalid condition » a une logique conditionnelle invalide")
- expect(page).to have_text("Le champ « header sections must have consistent order » devrait être précédé d'un titre de niveau 1")
- # TODO, test attestation_template, initiated_mail, :received_mail, :closed_mail, :refused_mail, :without_continuation_mail, :re_instructed_mail
+ 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_content("doit comporter au moins un choix sélectionnable")
+
+ expect(page).to have_selector("a", text: "repetition requires children")
+ expect(page).to have_content("doit comporter au moins un champ répétable")
+
+ expect(page).to have_selector("a", text: "invalid condition")
+ expect(page).to have_content("a une logique conditionnelle invalide")
+
+ expect(page).to have_selector("a", text: "header sections must have consistent order")
+ expect(page).to have_content("devrait être précédé d'un titre de niveau 1")
end
end
describe 'render error for other kind of associated objects' do
+ include Logic
+
let(:validation_context) { :publication }
let(:procedure) { create(:procedure, attestation_template:, initiated_mail:) }
let(:attestation_template) { build(:attestation_template) }
@@ -69,12 +83,15 @@ describe Procedure::ErrorsSummary, type: :component do
before do
[:attestation_template, :initiated_mail].map { procedure.send(_1).update_column(:body, '--invalidtag--') }
+ procedure.draft_revision.update(ineligibilite_enabled: true, ineligibilite_rules: ds_eq(constant(true), constant(1)), ineligibilite_message: 'ko')
subject
end
it 'render error nicely' do
- expect(page).to have_text("Le modèle d’attestation n'est pas valide")
- expect(page).to have_text("L’email de notification de passage de dossier en instruction n'est pas valide")
+ expect(page).to have_selector("a", text: "Les règles d’inéligibilité")
+ expect(page).to have_selector("a", text: "Le modèle d’attestation")
+ expect(page).to have_selector("a", text: "L’email de notification de passage de dossier en instruction")
+ expect(page).to have_text("n'est pas valide", count: 2)
end
end
end
diff --git a/spec/components/procedures/pending_republish_component_spec.rb b/spec/components/procedures/pending_republish_component_spec.rb
new file mode 100644
index 000000000..a5e301a20
--- /dev/null
+++ b/spec/components/procedures/pending_republish_component_spec.rb
@@ -0,0 +1,14 @@
+describe Procedure::PendingRepublishComponent, type: :component do
+ subject { render_inline(described_class.new(render_if:, procedure: build(:procedure, id: 1))) }
+ let(:page) { subject }
+ describe 'render_if' do
+ context 'when false' do
+ let(:render_if) { false }
+ it { expect(page).not_to have_text('Ces modifications ne seront appliquées') }
+ end
+ context 'when true' do
+ let(:render_if) { true }
+ it { expect(page).to have_text('Ces modifications ne seront appliquées') }
+ end
+ end
+end
diff --git a/spec/components/types_de_champ_editor/champ_component_spec.rb b/spec/components/types_de_champ_editor/champ_component_spec.rb
index 1e368f1ba..27b57472f 100644
--- a/spec/components/types_de_champ_editor/champ_component_spec.rb
+++ b/spec/components/types_de_champ_editor/champ_component_spec.rb
@@ -2,10 +2,12 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
describe 'render' do
let(:component) { described_class.new(coordinate:, upper_coordinates: []) }
let(:routing_rules_stable_ids) { [] }
+ let(:ineligibilite_rules_used?) { false }
before do
Flipper.enable_actor(:engagement_juridique_type_de_champ, procedure)
allow_any_instance_of(Procedure).to receive(:stable_ids_used_by_routing_rules).and_return(routing_rules_stable_ids)
+ allow_any_instance_of(ProcedureRevisionTypeDeChamp).to receive(:used_by_ineligibilite_rules?).and_return(ineligibilite_rules_used?)
render_inline(component)
end
@@ -29,6 +31,15 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
expect(page).to have_text(/utilisé pour\nle routage/)
end
end
+
+ context 'drop down tdc used for ineligibilite_rules' do
+ let(:ineligibilite_rules_used?) { true }
+
+ it do
+ expect(page).to have_css("select[disabled=\"disabled\"]")
+ expect(page).to have_text(/l’eligibilité des dossiers/)
+ end
+ end
end
describe 'tdc ej' do
diff --git a/spec/components/types_de_champ_editor/editor_component_spec.rb b/spec/components/types_de_champ_editor/editor_component_spec.rb
index 7b4a19e46..fb7094983 100644
--- a/spec/components/types_de_champ_editor/editor_component_spec.rb
+++ b/spec/components/types_de_champ_editor/editor_component_spec.rb
@@ -10,16 +10,18 @@ describe TypesDeChampEditor::EditorComponent, type: :component do
context 'types_de_champ_public' do
let(:is_annotation) { false }
it 'does not render private champs errors' do
- expect(subject).not_to have_text("« private » doit comporter au moins un choix sélectionnable")
- expect(subject).to have_text("« public » doit comporter au moins un choix sélectionnable")
+ expect(subject).not_to have_text("private")
+ expect(subject).to have_selector("a", text: "public")
+ expect(subject).to have_text("doit comporter au moins un choix sélectionnable")
end
end
context 'types_de_champ_private' do
let(:is_annotation) { true }
it 'does not render public champs errors' do
- expect(subject).to have_text("« private » doit comporter au moins un choix sélectionnable")
- expect(subject).not_to have_text("« public » doit comporter au moins un choix sélectionnable")
+ expect(subject).to have_selector("a", text: "private")
+ expect(subject).to have_text("doit comporter au moins un choix sélectionnable")
+ expect(subject).not_to have_text("public")
end
end
end
diff --git a/spec/controllers/administrateurs/ineligibilite_rules_controller_spec.rb b/spec/controllers/administrateurs/ineligibilite_rules_controller_spec.rb
new file mode 100644
index 000000000..2a76a054a
--- /dev/null
+++ b/spec/controllers/administrateurs/ineligibilite_rules_controller_spec.rb
@@ -0,0 +1,231 @@
+describe Administrateurs::IneligibiliteRulesController, type: :controller do
+ include Logic
+ let(:user) { create(:user) }
+ let(:admin) { create(:administrateur, user: create(:user)) }
+ let(:procedure) { create(:procedure, administrateurs: [admin], types_de_champ_public:) }
+ let(:types_de_champ_public) { [] }
+
+ describe 'condition management' do
+ before { sign_in(admin.user) }
+
+ let(:default_params) do
+ {
+ procedure_id: procedure.id,
+ revision_id: procedure.draft_revision.id
+ }
+ end
+
+ describe '#add_row' do
+ subject { post :add_row, params: default_params, format: :turbo_stream }
+
+ context 'without any row' do
+ it 'creates an empty condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(empty_operator(empty, empty))
+ end
+ end
+
+ context 'with row' do
+ before do
+ procedure.draft_revision.ineligibilite_rules = empty_operator(empty, empty)
+ procedure.draft_revision.save!
+ end
+
+ it 'add one more creates an empty condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(empty_operator(empty, empty))
+ .to(ds_and([
+ empty_operator(empty, empty),
+ empty_operator(empty, empty)
+ ]))
+ end
+ end
+ end
+
+ describe 'delete_row' do
+ let(:condition_form) do
+ {
+ top_operator_name: Logic::And.name,
+ rows: [
+ {
+ targeted_champ: empty.to_json,
+ operator_name: Logic::EmptyOperator,
+ value: empty.to_json
+ },
+ {
+ targeted_champ: empty.to_json,
+ operator_name: Logic::EmptyOperator,
+ value: empty.to_json
+ }
+ ]
+ }
+ end
+ let(:initial_condition) do
+ ds_and([
+ empty_operator(empty, empty),
+ empty_operator(empty, empty)
+ ])
+ end
+
+ subject { delete :delete_row, params: default_params.merge(row_index: 0, procedure_revision: { condition_form: }), format: :turbo_stream }
+ it 'remove condition' do
+ procedure.draft_revision.update(ineligibilite_rules: initial_condition)
+
+ expect { subject }
+ .to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(initial_condition)
+ .to(empty_operator(empty, empty))
+ end
+ end
+
+ context 'simple tdc' do
+ let(:types_de_champ_public) { [{ type: :yes_no }] }
+ let(:yes_no_tdc) { procedure.draft_revision.types_de_champ_for(scope: :public).first }
+ let(:targeted_champ) { champ_value(yes_no_tdc.stable_id).to_json }
+
+ describe '#change_targeted_champ' do
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: Logic::Eq.name,
+ value: constant(true).to_json
+ }
+ ]
+ }
+ end
+ subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
+ it 'update condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+
+ describe '#update' do
+ let(:value) { constant(true).to_json }
+ let(:operator_name) { Logic::Eq.name }
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: operator_name,
+ value: value
+ }
+ ]
+ }
+ end
+ subject { patch :update, params: default_params.merge(procedure_revision: { condition_form: condition_form }), format: :turbo_stream }
+ it 'updates condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+ end
+
+ context 'repetition tdc' do
+ let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :yes_no }] }] }
+ let(:yes_no_tdc) { procedure.draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'yes_no' } }
+ let(:targeted_champ) { champ_value(yes_no_tdc.stable_id).to_json }
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: Logic::Eq.name,
+ value: constant(true).to_json
+ }
+ ]
+ }
+ end
+ subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
+ describe "#update" do
+ it 'update condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+
+ describe '#change_targeted_champ' do
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: Logic::Eq.name,
+ value: constant(true).to_json
+ }
+ ]
+ }
+ end
+ subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
+ it 'update condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+ end
+ end
+
+ describe '#edit' do
+ subject { get :edit, params: { procedure_id: procedure.id } }
+
+ context 'when user is not signed in' do
+ it { is_expected.to redirect_to(new_user_session_path) }
+ end
+
+ context 'when user is signed in but not admin of procedure' do
+ before { sign_in(user) }
+ it { is_expected.to redirect_to(new_user_session_path) }
+ end
+
+ context 'when user is signed as admin' do
+ before do
+ sign_in(admin.user)
+ subject
+ end
+
+ it { is_expected.to have_http_status(200) }
+
+ context 'rendered without tdc' do
+ let(:types_de_champ_public) { [] }
+ render_views
+
+ it { expect(response.body).to have_link("Ajouter un champ supportant les conditions d’inéligibilité") }
+ end
+
+ context 'rendered with tdc' do
+ let(:types_de_champ_public) { [{ type: :yes_no }] }
+ render_views
+
+ it { expect(response.body).not_to have_link("Ajouter un champ supportant les conditions d’inéligibilité") }
+ end
+ end
+ end
+
+ describe 'change' do
+ let(:params) do
+ {
+ procedure_id: procedure.id,
+ procedure_revision: {
+ ineligibilite_message: 'panpan',
+ ineligibilite_enabled: '1'
+ }
+ }
+ end
+ before { sign_in(admin.user) }
+ it 'works' do
+ patch :change, params: params
+ draft_revision = procedure.reload.draft_revision
+ expect(draft_revision.ineligibilite_message).to eq('panpan')
+ expect(draft_revision.ineligibilite_enabled).to eq(true)
+ expect(response).to redirect_to(edit_admin_procedure_ineligibilite_rules_path(procedure))
+ end
+ end
+end
diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb
index 220bd6722..2a38eb7d7 100644
--- a/spec/controllers/users/dossiers_controller_spec.rb
+++ b/spec/controllers/users/dossiers_controller_spec.rb
@@ -398,7 +398,9 @@ describe Users::DossiersController, type: :controller do
describe '#submit_brouillon' do
before { sign_in(user) }
- let!(:dossier) { create(:dossier, user: user) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :text }] }
+ let!(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:anchor_to_first_champ) { controller.helpers.link_to first_champ.libelle, brouillon_dossier_path(anchor: first_champ.labelledby_id), class: 'error-anchor' }
let(:value) { 'beautiful value' }
@@ -439,9 +441,9 @@ describe Users::DossiersController, type: :controller do
render_views
let(:error_message) { 'nop' }
before do
- expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
- expect_any_instance_of(Dossier).to receive(:errors).and_return(
- [double(inner_error: double(base: first_champ), message: 'nop')]
+ allow_any_instance_of(Dossier).to receive(:validate).and_return(false)
+ allow_any_instance_of(Dossier).to receive(:errors).and_return(
+ [instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')]
)
subject
end
@@ -461,11 +463,8 @@ describe Users::DossiersController, type: :controller do
render_views
let(:value) { nil }
-
- before do
- first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
- subject
- end
+ let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] }
+ before { subject }
it { expect(response).to render_template(:brouillon) }
it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") }
@@ -548,8 +547,8 @@ describe Users::DossiersController, type: :controller do
render_views
before do
- expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
- expect_any_instance_of(Dossier).to receive(:errors).and_return(
+ allow_any_instance_of(Dossier).to receive(:validate).and_return(false)
+ allow_any_instance_of(Dossier).to receive(:errors).and_return(
[double(inner_error: double(base: first_champ), message: 'nop')]
)
@@ -661,7 +660,8 @@ describe Users::DossiersController, type: :controller do
describe '#update brouillon' do
before { sign_in(user) }
- let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}, { type: :piece_justificative }]) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{}, { type: :piece_justificative }] }
let(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:piece_justificative_champ) { dossier.champs_public.last }
@@ -754,13 +754,66 @@ describe Users::DossiersController, type: :controller do
end
end
- it "debounce search terms indexation" do
- # dossier creation trigger a first indexation and flag,
- # so we we have to remove this flag
- dossier.debounce_index_search_terms_flag.remove
+ context 'having ineligibilite_rules setup' do
+ include Logic
+ render_views
- assert_enqueued_jobs(1, only: DossierIndexSearchTermsJob) do
- 3.times { patch :update, params: payload, format: :turbo_stream }
+ let(:types_de_champ_public) { [{ type: :text }, { type: :integer_number }] }
+ let(:text_champ) { dossier.champs_public.first }
+ let(:number_champ) { dossier.champs_public.last }
+ let(:submit_payload) do
+ {
+ id: dossier.id,
+ dossier: {
+ groupe_instructeur_id: dossier.groupe_instructeur_id,
+ champs_public_attributes: {
+ text_champ.public_id => {
+ with_public_id: true,
+ value: "hello world"
+ },
+ number_champ.public_id => {
+ with_public_id: true,
+ value:
+ }
+ }
+ }
+ }
+ end
+ let(:must_be_greater_than) { 10 }
+
+ before do
+ procedure.published_revision.update(
+ ineligibilite_enabled: true,
+ ineligibilite_message: 'lol',
+ ineligibilite_rules: greater_than(champ_value(number_champ.stable_id), constant(must_be_greater_than))
+ )
+ procedure.published_revision.save!
+ end
+ render_views
+
+ context 'when it switches from true to false' do
+ let(:value) { must_be_greater_than + 1 }
+
+ it 'raises popup' do
+ subject
+ dossier.reload
+ expect(dossier.can_passer_en_construction?).to be_falsey
+ expect(assigns(:can_passer_en_construction_was)).to eq(true)
+ expect(assigns(:can_passer_en_construction_is)).to eq(false)
+ expect(response.body).to match(ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken))
+ end
+ end
+
+ context 'when it stays true' do
+ let(:value) { must_be_greater_than - 1 }
+ it 'does nothing' do
+ subject
+ dossier.reload
+ expect(dossier.can_passer_en_construction?).to be_truthy
+ expect(assigns(:can_passer_en_construction_was)).to eq(true)
+ expect(assigns(:can_passer_en_construction_is)).to eq(true)
+ expect(response.body).not_to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}")
+ end
end
end
end
@@ -868,8 +921,8 @@ describe Users::DossiersController, type: :controller do
context 'classic error' do
before do
- expect_any_instance_of(Dossier).to receive(:save).and_return(false)
- expect_any_instance_of(Dossier).to receive(:errors).and_return(
+ allow_any_instance_of(Dossier).to receive(:save).and_return(false)
+ allow_any_instance_of(Dossier).to receive(:errors).and_return(
[message: 'nop', inner_error: double(base: first_champ)]
)
subject
diff --git a/spec/models/logic/binary_operator_spec.rb b/spec/models/logic/binary_operator_spec.rb
index e27c3b7bc..b7924ebc7 100644
--- a/spec/models/logic/binary_operator_spec.rb
+++ b/spec/models/logic/binary_operator_spec.rb
@@ -43,6 +43,8 @@ end
describe Logic::GreaterThanEq do
include Logic
+ let(:champ) { create(:champ_integer_number, value: nil) }
+
it 'computes' do
expect(greater_than_eq(constant(0), constant(1)).compute).to be(false)
expect(greater_than_eq(constant(1), constant(1)).compute).to be(true)
diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb
index 4c7a17ba3..434d82e44 100644
--- a/spec/models/procedure_revision_spec.rb
+++ b/spec/models/procedure_revision_spec.rb
@@ -347,306 +347,417 @@ describe ProcedureRevision do
end
end
- describe '#compare' do
+ describe '#compare_types_de_champ' do
include Logic
-
- let(:first_tdc) { draft.types_de_champ_public.first }
- let(:second_tdc) { draft.types_de_champ_public.second }
let(:new_draft) { procedure.create_new_revision }
+ subject { procedure.active_revision.compare_types_de_champ(new_draft.reload).map(&:to_h) }
- subject { procedure.active_revision.compare(new_draft.reload).map(&:to_h) }
+ describe 'when tdcs changes' do
+ let(:first_tdc) { draft.types_de_champ_public.first }
+ let(:second_tdc) { draft.types_de_champ_public.second }
- context 'with a procedure with 2 tdcs' do
- let(:procedure) do
- create(:procedure, types_de_champ_public: [
- { type: :integer_number, libelle: 'l1' },
- { type: :text, libelle: 'l2' }
- ])
+ context 'with a procedure with 2 tdcs' do
+ let(:procedure) do
+ create(:procedure, types_de_champ_public: [
+ { type: :integer_number, libelle: 'l1' },
+ { type: :text, libelle: 'l2' }
+ ])
+ end
+
+ context 'when a condition is added' do
+ before do
+ second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
+ second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ attribute: :condition,
+ from: nil,
+ label: "l2",
+ op: :update,
+ private: false,
+ stable_id: second_tdc.stable_id,
+ to: "(l1 == 3)"
+ }
+ ])
+ end
+ end
+
+ context 'when a condition is removed' do
+ before do
+ second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
+ draft.reload
+
+ second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
+ second.update(condition: nil)
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ attribute: :condition,
+ from: "(l1 == 2)",
+ label: "l2",
+ op: :update,
+ private: false,
+ stable_id: second_tdc.stable_id,
+ to: nil
+ }
+ ])
+ end
+ end
+
+ context 'when a condition is changed' do
+ before do
+ second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
+ draft.reload
+
+ second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
+ second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ attribute: :condition,
+ from: "(l1 == 2)",
+ label: "l2",
+ op: :update,
+ private: false,
+ stable_id: second_tdc.stable_id,
+ to: "(l1 == 3)"
+ }
+ ])
+ end
+ end
end
- context 'when a condition is added' do
- before do
- second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
- second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
+ context 'when a type de champ is added' do
+ let(:procedure) { create(:procedure) }
+ let(:new_tdc) do
+ new_draft.add_type_de_champ(
+ type_champ: TypeDeChamp.type_champs.fetch(:text),
+ libelle: "Un champ text"
+ )
end
+ before { new_tdc }
+
it do
is_expected.to eq([
{
- attribute: :condition,
- from: nil,
- label: "l2",
- op: :update,
+ op: :add,
+ label: "Un champ text",
private: false,
- stable_id: second_tdc.stable_id,
- to: "(l1 == 3)"
+ mandatory: false,
+ stable_id: new_tdc.stable_id
}
])
end
end
- context 'when a condition is removed' do
- before do
- second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
- draft.reload
+ context 'when a type de champ is changed' do
+ context 'when libelle, description, and mandatory are changed' do
+ let(:procedure) { create(:procedure, :with_type_de_champ) }
- second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
- second.update(condition: nil)
+ before do
+ updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
+
+ updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ op: :update,
+ attribute: :libelle,
+ label: first_tdc.libelle,
+ private: false,
+ from: first_tdc.libelle,
+ to: "modifier le libelle",
+ stable_id: first_tdc.stable_id
+ },
+ {
+ op: :update,
+ attribute: :description,
+ label: first_tdc.libelle,
+ private: false,
+ from: first_tdc.description,
+ to: "une description",
+ stable_id: first_tdc.stable_id
+ },
+ {
+ op: :update,
+ attribute: :mandatory,
+ label: first_tdc.libelle,
+ private: false,
+ from: false,
+ to: true,
+ stable_id: first_tdc.stable_id
+ }
+ ])
+ end
+ end
+
+ context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) }
+
+ before do
+ updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
+
+ updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
+ end
+ it do
+ is_expected.to eq([
+ {
+ op: :update,
+ attribute: :collapsible_explanation_enabled,
+ label: first_tdc.libelle,
+ private: first_tdc.private?,
+ from: false,
+ to: true,
+ stable_id: first_tdc.stable_id
+ },
+ {
+ op: :update,
+ attribute: :collapsible_explanation_text,
+ label: first_tdc.libelle,
+ private: first_tdc.private?,
+ from: nil,
+ to: 'afficher au clique',
+ stable_id: first_tdc.stable_id
+ }
+ ])
+ end
+ end
+ end
+
+ context 'when a type de champ is moved' do
+ 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_third_tdc) { new_draft.types_de_champ_public.third }
+
+ before do
+ new_draft_second_tdc
+ new_draft_third_tdc
+ new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2)
end
it do
is_expected.to eq([
{
- attribute: :condition,
- from: "(l1 == 2)",
- label: "l2",
- op: :update,
+ op: :move,
+ label: new_draft_third_tdc.libelle,
private: false,
- stable_id: second_tdc.stable_id,
- to: nil
- }
- ])
- end
- end
-
- context 'when a condition is changed' do
- before do
- second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
- draft.reload
-
- second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
- second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
- end
-
- it do
- is_expected.to eq([
+ from: 2,
+ to: 1,
+ stable_id: new_draft_third_tdc.stable_id
+ },
{
- attribute: :condition,
- from: "(l1 == 2)",
- label: "l2",
- op: :update,
+ op: :move,
+ label: new_draft_second_tdc.libelle,
private: false,
- stable_id: second_tdc.stable_id,
- to: "(l1 == 3)"
+ from: 1,
+ to: 2,
+ stable_id: new_draft_second_tdc.stable_id
}
])
end
end
- end
- context 'when a type de champ is added' do
- let(:procedure) { create(:procedure) }
- let(:new_tdc) do
- new_draft.add_type_de_champ(
- type_champ: TypeDeChamp.type_champs.fetch(:text),
- libelle: "Un champ text"
- )
- end
-
- before { new_tdc }
-
- it do
- is_expected.to eq([
- {
- op: :add,
- label: "Un champ text",
- private: false,
- mandatory: false,
- stable_id: new_tdc.stable_id
- }
- ])
- end
- end
-
- context 'when a type de champ is changed' do
- context 'when libelle, description, and mandatory are changed' do
+ context 'when a type de champ is removed' do
let(:procedure) { create(:procedure, :with_type_de_champ) }
before do
- updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
-
- updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
+ new_draft.remove_type_de_champ(first_tdc.stable_id)
end
it do
is_expected.to eq([
{
- op: :update,
- attribute: :libelle,
+ op: :remove,
label: first_tdc.libelle,
private: false,
- from: first_tdc.libelle,
- to: "modifier le libelle",
- stable_id: first_tdc.stable_id
- },
- {
- op: :update,
- attribute: :description,
- label: first_tdc.libelle,
- private: false,
- from: first_tdc.description,
- to: "une description",
- stable_id: first_tdc.stable_id
- },
- {
- op: :update,
- attribute: :mandatory,
- label: first_tdc.libelle,
- private: false,
- from: false,
- to: true,
stable_id: first_tdc.stable_id
}
])
end
end
- context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) }
+ context 'when a child type de champ is transformed into a drop_down_list' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
before do
- updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
-
- updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
+ child = new_draft.children_of(new_draft.types_de_champ_public.last).first
+ new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :drop_down_list, drop_down_options: ['one', 'two'])
end
+
it do
is_expected.to eq([
{
op: :update,
- attribute: :collapsible_explanation_enabled,
- label: first_tdc.libelle,
- private: first_tdc.private?,
- from: false,
- to: true,
- stable_id: first_tdc.stable_id
+ attribute: :type_champ,
+ label: "sub type de champ",
+ private: false,
+ from: "text",
+ to: "drop_down_list",
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
},
{
op: :update,
- attribute: :collapsible_explanation_text,
- label: first_tdc.libelle,
- private: first_tdc.private?,
- from: nil,
- to: 'afficher au clique',
- stable_id: first_tdc.stable_id
+ attribute: :drop_down_options,
+ label: "sub type de champ",
+ private: false,
+ from: [],
+ to: ["one", "two"],
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
+ }
+ ])
+ end
+ end
+
+ context 'when a child type de champ is transformed into a map' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
+
+ before do
+ child = new_draft.children_of(new_draft.types_de_champ_public.last).first
+ new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :carte, options: { cadastres: true, znieff: true })
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ op: :update,
+ attribute: :type_champ,
+ label: "sub type de champ",
+ private: false,
+ from: "text",
+ to: "carte",
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
+ },
+ {
+ op: :update,
+ attribute: :carte_layers,
+ label: "sub type de champ",
+ private: false,
+ from: [],
+ to: [:cadastres, :znieff],
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
}
])
end
end
end
+ end
- context 'when a type de champ is moved' do
- 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_third_tdc) { new_draft.types_de_champ_public.third }
+ describe 'compare_ineligibilite_rules' do
+ include Logic
+ let(:new_draft) { procedure.create_new_revision }
+ subject { procedure.active_revision.compare_ineligibilite_rules(new_draft.reload) }
- before do
- new_draft_second_tdc
- new_draft_third_tdc
- new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2)
+ context 'when ineligibilite_rules changes' do
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :yes_no }] }
+ let(:yes_no_tdc) { new_draft.types_de_champ_public.first }
+
+ context 'when nothing changed' do
+ it { is_expected.to be_empty }
end
- it do
- is_expected.to eq([
- {
- op: :move,
- label: new_draft_third_tdc.libelle,
- private: false,
- from: 2,
- to: 1,
- stable_id: new_draft_third_tdc.stable_id
- },
- {
- op: :move,
- label: new_draft_second_tdc.libelle,
- private: false,
- from: 1,
- to: 2,
- stable_id: new_draft_second_tdc.stable_id
- }
- ])
+ context 'when ineligibilite_rules added' do
+ before do
+ new_draft.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::AddEligibiliteRuleChange)) }
+ end
+
+ context 'when ineligibilite_rules removed' do
+ before do
+ procedure.published_revision.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::RemoveEligibiliteRuleChange)) }
+ end
+
+ context 'when ineligibilite_rules changed' do
+ before do
+ procedure.published_revision.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ new_draft.update!(ineligibilite_rules: ds_and([
+ ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)),
+ empty_operator(empty, empty)
+ ]))
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::UpdateEligibiliteRuleChange)) }
+ end
+
+ context 'when when ineligibilite_enabled changes from false to true' do
+ before do
+ procedure.published_revision.update!(ineligibilite_enabled: false, ineligibilite_message: :required)
+ new_draft.update!(ineligibilite_enabled: true, ineligibilite_message: :required)
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::EligibiliteEnabledChange)) }
+ end
+
+ context 'when ineligibilite_enabled changes from true to false' do
+ before do
+ procedure.published_revision.update!(ineligibilite_enabled: true, ineligibilite_message: :required)
+ new_draft.update!(ineligibilite_enabled: false, ineligibilite_message: :required)
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::EligibiliteDisabledChange)) }
+ end
+
+ context 'when ineligibilite_message changes' do
+ before do
+ procedure.published_revision.update!(ineligibilite_message: :a)
+ new_draft.update!(ineligibilite_message: :b)
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::UpdateEligibiliteMessageChange)) }
end
end
+ end
- context 'when a type de champ is removed' do
- let(:procedure) { create(:procedure, :with_type_de_champ) }
-
- before do
- new_draft.remove_type_de_champ(first_tdc.stable_id)
- end
-
- it do
- is_expected.to eq([
- {
- op: :remove,
- label: first_tdc.libelle,
- private: false,
- stable_id: first_tdc.stable_id
- }
- ])
- end
+ describe 'ineligibilite_rules_are_valid?' do
+ include Logic
+ let(:procedure) { create(:procedure) }
+ let(:draft_revision) { procedure.draft_revision }
+ let(:ineligibilite_message) { 'ok' }
+ let(:ineligibilite_enabled) { true }
+ before do
+ procedure.draft_revision.update(ineligibilite_rules:, ineligibilite_message:, ineligibilite_enabled:)
end
- context 'when a child type de champ is transformed into a drop_down_list' do
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
-
- before do
- child = new_draft.children_of(new_draft.types_de_champ_public.last).first
- new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :drop_down_list, drop_down_options: ['one', 'two'])
- end
-
- it do
- is_expected.to eq([
- {
- op: :update,
- attribute: :type_champ,
- label: "sub type de champ",
- private: false,
- from: "text",
- to: "drop_down_list",
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- },
- {
- op: :update,
- attribute: :drop_down_options,
- label: "sub type de champ",
- private: false,
- from: [],
- to: ["one", "two"],
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- }
- ])
+ context 'when ineligibilite_rules are valid' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(true)) }
+ it 'is valid' do
+ expect(draft_revision.validate(:publication)).to be_truthy
+ expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_truthy
end
end
-
- context 'when a child type de champ is transformed into a map' do
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
-
- before do
- child = new_draft.children_of(new_draft.types_de_champ_public.last).first
- new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :carte, options: { cadastres: true, znieff: true })
+ context 'when ineligibilite_rules are invalid on simple champ' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
+ it 'is invalid' do
+ expect(draft_revision.validate(:publication)).to be_falsey
+ expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
end
-
- it do
- is_expected.to eq([
- {
- op: :update,
- attribute: :type_champ,
- label: "sub type de champ",
- private: false,
- from: "text",
- to: "carte",
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- },
- {
- op: :update,
- attribute: :carte_layers,
- label: "sub type de champ",
- private: false,
- from: [],
- to: [:cadastres, :znieff],
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- }
- ])
+ end
+ context 'when ineligibilite_rules are invalid on repetition champ' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
+ let(:procedure) { create(:procedure, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :integer_number }] }] }
+ let(:tdc_number) { draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'integer_number' } }
+ let(:ineligibilite_rules) do
+ ds_eq(champ_value(tdc_number.stable_id), constant(true))
+ end
+ it 'is invalid' do
+ expect(draft_revision.validate(:publication)).to be_falsey
+ expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
end
end
end
diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb
index 2a20829ab..0fa94a425 100644
--- a/spec/models/procedure_spec.rb
+++ b/spec/models/procedure_spec.rb
@@ -372,12 +372,12 @@ describe Procedure do
]
end
let(:types_de_champ_private) { [] }
- let(:invalid_repetition_error_message) { 'Le champ « Enfants » doit comporter au moins un champ répétable' }
- let(:invalid_drop_down_error_message) { 'Le champ « Civilité » doit comporter au moins un choix sélectionnable' }
+ let(:invalid_repetition_error_message) { "doit comporter au moins un champ répétable" }
+ let(:invalid_drop_down_error_message) { "doit comporter au moins un choix sélectionnable" }
it 'validates that no repetition type de champ is empty' do
procedure.validate(:publication)
- expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).to include(invalid_repetition_error_message)
+ expect(procedure.errors.messages_for(:draft_types_de_champ_public)).to include(invalid_repetition_error_message)
new_draft = procedure.draft_revision
repetition = procedure.draft_revision.types_de_champ_public.find(&:repetition?)
@@ -385,17 +385,17 @@ describe Procedure do
new_draft.revision_types_de_champ.create(type_de_champ: create(:type_de_champ), position: 0, parent: parent_coordinate)
procedure.validate(:publication)
- expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).not_to include(invalid_repetition_error_message)
+ expect(procedure.errors.messages_for(:draft_types_de_champ_public)).not_to include(invalid_repetition_error_message)
end
it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication)
- expect(procedure.errors.full_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_list_value: "--title--\r\nsome value")
procedure.reload.validate(:publication)
- expect(procedure.errors.full_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)
end
end
@@ -408,17 +408,21 @@ describe Procedure do
end
let(:types_de_champ_public) { [] }
- let(:invalid_repetition_error_message) { 'L’annotation privée « Enfants » doit comporter au moins un champ répétable' }
- let(:invalid_drop_down_error_message) { 'L’annotation privée « Civilité » doit comporter au moins un choix sélectionnable' }
+ let(:invalid_repetition_error_message) { "doit comporter au moins un champ répétable" }
+ let(:invalid_drop_down_error_message) { "doit comporter au moins un choix sélectionnable" }
it 'validates that no repetition type de champ is empty' do
procedure.validate(:publication)
- expect(procedure.errors.full_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?)
+ expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(repetition)
end
it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication)
- expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to include(invalid_drop_down_error_message)
+ 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?)
+ expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(drop_down)
end
end
@@ -441,7 +445,7 @@ describe Procedure do
include Logic
let(:types_de_champ_public) { [{ type: :text, libelle: 'condition', condition: ds_eq(champ_value(1), constant(2)), stable_id: 2 }] }
let(:types_de_champ_private) { [{ type: :decimal_number, stable_id: 1 }] }
- let(:error_on_condition) { "Le champ « condition » a une logique conditionnelle invalide" }
+ let(:error_on_condition) { "Le champ a une logique conditionnelle invalide" }
it 'validate without context' do
procedure.validate
diff --git a/spec/system/administrateurs/procedure_ineligibilite_spec.rb b/spec/system/administrateurs/procedure_ineligibilite_spec.rb
new file mode 100644
index 000000000..e93a6ea5d
--- /dev/null
+++ b/spec/system/administrateurs/procedure_ineligibilite_spec.rb
@@ -0,0 +1,45 @@
+describe 'Administrateurs can edit procedures', js: true do
+ include Logic
+
+ let(:procedure) { create(:procedure, administrateurs: [create(:administrateur)]) }
+ before do
+ login_as procedure.administrateurs.first.user, scope: :user
+ end
+
+ scenario 'setup eligibilite' do
+ # explain no champ compatible
+ visit admin_procedure_path(procedure)
+ expect(page).to have_content("Désactivé")
+
+ # explain which champs are compatible
+ visit edit_admin_procedure_ineligibilite_rules_path(procedure)
+ expect(page).to have_content("Inéligibilité des dossiers")
+ expect(page).to have_content("Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire : ")
+ click_on "Ajouter un champ supportant les conditions d’inéligibilité"
+
+ # setup a compatible champ
+ expect(page).to have_content('Champs du formulaire')
+ click_on 'Ajouter un champ'
+ select "Oui/Non"
+ fill_in "Libellé du champ", with: "Un champ oui non"
+ click_on "Revenir à l'écran de gestion"
+ procedure.reload
+ first_tdc = procedure.draft_revision.types_de_champ.first
+ # back to procedure dashboard, explain you can set it up now
+ expect(page).to have_content('À configurer')
+ visit edit_admin_procedure_ineligibilite_rules_path(procedure)
+
+ # setup rules and stuffs
+ expect(page).to have_content("Inéligibilité des dossiers")
+ fill_in "Message d’inéligibilité", with: "vous n'etes pas eligible"
+ find('label', text: 'Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité').click
+ click_on "Ajouter une règle d’inéligibilité"
+ all('select').first.select 'Un champ oui non'
+ click_on 'Enregistrer'
+
+ # rules are setup
+ wait_until { procedure.reload.draft_revision.ineligibilite_enabled == true }
+ expect(procedure.draft_revision.ineligibilite_message).to eq("vous n'etes pas eligible")
+ expect(procedure.draft_revision.ineligibilite_rules).to eq(ds_eq(champ_value(first_tdc.stable_id), constant(true)))
+ end
+end
diff --git a/spec/system/administrateurs/procedure_publish_spec.rb b/spec/system/administrateurs/procedure_publish_spec.rb
index 10818971c..dc1858358 100644
--- a/spec/system/administrateurs/procedure_publish_spec.rb
+++ b/spec/system/administrateurs/procedure_publish_spec.rb
@@ -72,8 +72,8 @@ describe 'Publishing a procedure', js: true do
visit admin_procedure_path(procedure)
expect(page).to have_content('Des problèmes empêchent la publication de la démarche')
- expect(page).to have_content("« Enfants » doit comporter au moins un champ répétable")
- expect(page).to have_content("« Civilité » doit comporter au moins un choix sélectionnable")
+ expect(page).to have_content("Enfants doit comporter au moins un champ répétable")
+ expect(page).to have_content("Civilité doit comporter au moins un choix sélectionnable")
visit admin_procedure_publication_path(procedure)
expect(find_field('procedure_path').value).to eq procedure.path
@@ -195,7 +195,7 @@ describe 'Publishing a procedure', js: true do
scenario 'an error message prevents the publication' do
visit admin_procedure_path(procedure)
expect(page).to have_content('Des problèmes empêchent la publication des modifications')
- expect(page).to have_link('corriger', href: edit_admin_procedure_mail_template_path(procedure, Mails::InitiatedMail::SLUG))
+ expect(page).to have_link(href: edit_admin_procedure_mail_template_path(procedure, Mails::InitiatedMail::SLUG))
expect(page).to have_button('Publier les modifications', disabled: true)
end
end
diff --git a/spec/system/users/dossier_ineligibilite_spec.rb b/spec/system/users/dossier_ineligibilite_spec.rb
new file mode 100644
index 000000000..5bbb25c75
--- /dev/null
+++ b/spec/system/users/dossier_ineligibilite_spec.rb
@@ -0,0 +1,182 @@
+require 'system/users/dossier_shared_examples.rb'
+
+describe 'Dossier Inéligibilité', js: true do
+ include Logic
+
+ let(:user) { create(:user) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:dossier) { create(:dossier, procedure:, user:) }
+
+ let(:published_revision) { procedure.published_revision }
+ let(:first_tdc) { published_revision.types_de_champ.first }
+ let(:second_tdc) { published_revision.types_de_champ.second }
+ let(:ineligibilite_message) { 'sry vous pouvez aps soumettre votre dossier' }
+ let(:eligibilite_params) { { ineligibilite_enabled: true, ineligibilite_message: } }
+
+ before do
+ published_revision.update(eligibilite_params.merge(ineligibilite_rules:))
+ login_as user, scope: :user
+ end
+
+ describe 'ineligibilite_rules with a single BinaryOperator' do
+ let(:types_de_champ_public) { [{ type: :yes_no, stable_id: 1 }] }
+ let(:ineligibilite_rules) { ds_eq(champ_value(first_tdc.stable_id), constant(true)) }
+
+ scenario 'can submit, can not submit, reload' do
+ visit brouillon_dossier_path(dossier)
+ # no error while dossier is empty
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # does raise error when dossier is filled with condition that does not match
+ within "#champ-1" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # raise error when dossier is filled with condition that matches
+ within "#champ-1" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: true)
+ expect(page).to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # reload page and see error
+ visit brouillon_dossier_path(dossier)
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: true)
+ expect(page).to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # modal is closable, and we can change our dossier response to be eligible
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ within "#champ-1" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+
+ # it works, yay
+ click_on "Déposer le dossier"
+ wait_until { dossier.reload.en_construction? == true }
+ end
+ end
+
+ describe 'ineligibilite_rules with a Or' do
+ let(:types_de_champ_public) { [{ type: :yes_no, libelle: 'l1' }, { type: :drop_down_list, libelle: 'l2', options: ['Paris', 'Marseille'] }] }
+ let(:ineligibilite_rules) do
+ ds_or([
+ ds_eq(champ_value(first_tdc.stable_id), constant(true)),
+ ds_eq(champ_value(second_tdc.stable_id), constant('Paris'))
+ ])
+ end
+
+ scenario 'can submit, can not submit, can edit, etc...' do
+ visit brouillon_dossier_path(dossier)
+ # no error while dossier is empty
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # first condition matches (so ineligible), cannot submit dossier and error message is clear
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: true)
+ expect(page).to have_content("Vous ne pouvez pas déposer votre dossier")
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ # first condition does not matches, I can conitnue
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+
+ # Now test dossier modification
+ click_on "Déposer le dossier"
+ click_on "Accéder à votre dossier"
+ click_on "Modifier le dossier"
+
+ # first matches, means i'm blocked to send my file.
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: true)
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: false)
+
+ # second condition matches, means i'm blocked to send my file
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Paris').click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: true)
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ # none of conditions matches, i can submit
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Marseille').click
+ end
+
+ # it works, yay
+ click_on "Déposer les modifications"
+ wait_until { dossier.reload.en_construction? == true }
+ end
+ end
+
+ describe 'ineligibilite_rules with a And and all visible champs' do
+ let(:types_de_champ_public) { [{ type: :yes_no, libelle: 'l1' }, { type: :drop_down_list, libelle: 'l2', options: ['Paris', 'Marseille'] }] }
+ let(:ineligibilite_rules) do
+ ds_and([
+ ds_eq(champ_value(first_tdc.stable_id), constant(true)),
+ ds_eq(champ_value(second_tdc.stable_id), constant('Paris'))
+ ])
+ end
+
+ scenario 'can submit, can not submit, can edit, etc...' do
+ visit brouillon_dossier_path(dossier)
+ # no error while dossier is empty
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # only one condition is matches, can submit dossier
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # Now test dossier modification
+ click_on "Déposer le dossier"
+ click_on "Accéder à votre dossier"
+ click_on "Modifier le dossier"
+
+ # second condition matches, means i'm blocked to send my file
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Paris').click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: true)
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ # none of conditions matches, i can submit
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Marseille').click
+ end
+
+ # it works, yay
+ click_on "Déposer les modifications"
+ wait_until { dossier.reload.en_construction? == true }
+ end
+ end
+end
diff --git a/spec/views/shared/dossiers/_edit.html.haml_spec.rb b/spec/views/shared/dossiers/_edit.html.haml_spec.rb
index c242f3ec8..f6ce8f5bf 100644
--- a/spec/views/shared/dossiers/_edit.html.haml_spec.rb
+++ b/spec/views/shared/dossiers/_edit.html.haml_spec.rb
@@ -149,4 +149,17 @@ describe 'shared/dossiers/edit', type: :view do
end
end
end
+
+ context 'when dossier transitions rules are computable and passer_en_construction is false' do
+ let(:types_de_champ_public) { [] }
+ let(:dossier) { create(:dossier, procedure:) }
+
+ before do
+ allow(dossier).to receive(:can_passer_en_construction?).and_return(false)
+ end
+
+ it 'renders broken transitions rules dialog' do
+ expect(subject).to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}")
+ end
+ end
end