Merge pull request #10292 from mfo/US/elligible-on-submit

ETQ administrateur, je peux ajouter des conditions d'eligibilité auxquelles les dossiers doivent correspondre sans quoi l'usager ne peut déposer son dossier
This commit is contained in:
mfo 2024-06-11 09:46:25 +00:00 committed by GitHub
commit f6a5e932b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 1772 additions and 387 deletions

View file

@ -57,5 +57,14 @@ form.form > .conditionnel {
select.alert { select.alert {
border-color: $dark-red; border-color: $dark-red;
} }
&:first-child {
padding-left: 0;
}
&:last-child {
text-align: right;
padding-right: 0;
}
} }
} }

View file

@ -61,7 +61,7 @@ class Conditions::ConditionsComponent < ApplicationComponent
def available_targets_for_select def available_targets_for_select
@source_tdcs @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] } .map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
end end

View file

@ -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

View file

@ -0,0 +1,6 @@
---
fr:
display_if: Bloquer si
select: Sélectionner
add_condition: Ajouter une règle dinéligibilité
remove_a_row: Supprimer une règle

View file

@ -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 diné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 diné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'

View file

@ -1,4 +1,6 @@
class Dossiers::EditFooterComponent < ApplicationComponent class Dossiers::EditFooterComponent < ApplicationComponent
delegate :can_passer_en_construction?, to: :@dossier
def initialize(dossier:, annotation:) def initialize(dossier:, annotation:)
@dossier = dossier @dossier = dossier
@annotation = annotation @annotation = annotation
@ -14,20 +16,29 @@ class Dossiers::EditFooterComponent < ApplicationComponent
@annotation.present? @annotation.present?
end 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 def submit_draft_button_options
{ {
class: 'fr-btn fr-btn--sm', class: 'fr-btn fr-btn--sm',
disabled: !owner?, disabled: !owner? || !can_passer_en_construction?,
method: :post, method: :post,
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' } data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server }
} }
end end
def submit_en_construction_button_options def submit_en_construction_button_options
{ {
class: 'fr-btn fr-btn--sm', class: 'fr-btn fr-btn--sm',
disabled: !can_passer_en_construction?,
method: :post, 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" } form: { id: "form-submit-en-construction" }
} }
end end

View file

@ -2,5 +2,6 @@
en: en:
submit: Submit the file submit: Submit the file
submit_changes: Submit file changes submit_changes: Submit file changes
submit_disabled: File submission disabled
submitting: Submitting… submitting: Submitting…
invite_notice: You are invited to make amendments to this file but <strong>only the owner themselves can submit it</strong>. invite_notice: You are invited to make amendments to this file but <strong>only the owner themselves can submit it</strong>.

View file

@ -2,5 +2,6 @@
fr: fr:
submit: Déposer le dossier submit: Déposer le dossier
submit_changes: Déposer les modifications submit_changes: Déposer les modifications
submit_disabled: Pourquoi je ne peux pas déposer mon dossier ?
submitting: Envoi en cours… submitting: Envoi en cours…
invite_notice: En tant quinvité, vous pouvez remplir ce formulaire mais <strong>le titulaire du dossier doit le déposer lui-même</strong>. invite_notice: En tant quinvité, vous pouvez remplir ce formulaire mais <strong>le titulaire du dossier doit le déposer lui-même</strong>.

View file

@ -3,8 +3,13 @@
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?) = render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
- if !annotation? && @dossier.can_transition_to_en_construction? - 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 = 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 = button_to t('.submit_changes'), modifier_dossier_url(@dossier.editing_fork_origin), submit_en_construction_button_options

View file

@ -3,17 +3,15 @@
class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
ErrorDescriptor = Data.define(:anchor, :label, :error_message) ErrorDescriptor = Data.define(:anchor, :label, :error_message)
def initialize(dossier:, errors:) def initialize(dossier:)
@dossier = dossier @dossier = dossier
@errors = errors
end end
def dedup_and_partitioned_errors 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 .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 .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) } .map { |error| to_error_descriptor(error) }
yield(Array(formated_errors[0..2]), Array(formated_errors[3..]))
end end
def to_error_descriptor(error) def to_error_descriptor(error)
@ -27,6 +25,6 @@ class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
end end
def render? def render?
!@errors.empty? !@dossier.errors.empty?
end end
end end

View file

@ -5,4 +5,3 @@ en:
Your file has 1 error. <a href="%{url}">Fix-it</a> to continue : Your file has 1 error. <a href="%{url}">Fix-it</a> to continue :
other: | other: |
Your file has %{count} errors. <a href="%{url}">Fix-them</a> to continue : Your file has %{count} errors. <a href="%{url}">Fix-them</a> to continue :
see_more: Show all errors

View file

@ -5,4 +5,3 @@ fr:
Votre dossier contient 1 champ en erreur. <a href="%{url}">Corrigez-la</a> pour poursuivre : Votre dossier contient 1 champ en erreur. <a href="%{url}">Corrigez-la</a> pour poursuivre :
other: | other: |
Votre dossier contient %{count} champs en erreurs. <a href="%{url}">Corrigez-les</a> pour poursuivre : Votre dossier contient %{count} champs en erreurs. <a href="%{url}">Corrigez-les</a> pour poursuivre :
see_more: Afficher toutes les erreurs

View file

@ -1,15 +1,4 @@
.fr-alert.fr-alert--error.fr-mb-3w{ role: "alertdialog" } .fr-alert.fr-alert--error.fr-mb-3w{ role: "alertdialog" }
- dedup_and_partitioned_errors do |head, tail| - if dedup_and_partitioned_errors.size > 0
%p#sumup-errors= t('.sumup_html', count: head.size + tail.size, url: head.first.anchor) %p#sumup-errors= t('.sumup_html', count: dedup_and_partitioned_errors.size, url: dedup_and_partitioned_errors.first.anchor)
%ul.fr-mb-0#head-errors = render ExpandableErrorList.new(errors: dedup_and_partitioned_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})"

View file

@ -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

View file

@ -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"

View file

@ -0,0 +1,5 @@
fr:
modal:
title: "Vous ne pouvez pas déposer votre dossier"
close: "Fermer"
close_alt: "Fermer la fenêtre modale"

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
---
en:
see_more: Show all errors

View file

@ -0,0 +1,3 @@
---
fr:
see_more: Afficher toutes les erreurs

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,8 @@
---
fr:
title: Inéligibilité des dossiers
state:
pending: Désactivé
ready: À configurer
completed: Activé
subtitle: Gérez vos conditions dinéligibilité en fonction des champs du formulaire

View file

@ -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')

View file

@ -1,4 +1,6 @@
class Procedure::ErrorsSummary < ApplicationComponent class Procedure::ErrorsSummary < ApplicationComponent
ErrorDescriptor = Data.define(:anchor, :label, :error_message)
def initialize(procedure:, validation_context:) def initialize(procedure:, validation_context:)
@procedure = procedure @procedure = procedure
@validation_context = validation_context @validation_context = validation_context
@ -24,14 +26,14 @@ class Procedure::ErrorsSummary < ApplicationComponent
@procedure.errors.present? @procedure.errors.present?
end end
def error_messages def errors
@procedure.errors.map do |error| @procedure.errors.map { to_error_descriptor(_1) }
[error, error_correction_page(error)]
end
end end
def error_correction_page(error) def error_correction_page(error)
case error.attribute case error.attribute
when :ineligibilite_rules
edit_admin_procedure_ineligibilite_rules_path(@procedure)
when :draft_types_de_champ_public when :draft_types_de_champ_public
tdc = error.options[:type_de_champ] tdc = error.options[:type_de_champ]
champs_admin_procedure_path(@procedure, anchor: dom_id(tdc.stable_self, :editor_error)) 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)) edit_admin_procedure_mail_template_path(@procedure, klass.const_get(:SLUG))
end end
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 end

View file

@ -2,8 +2,4 @@
- if invalid? - if invalid?
= render Dsfr::AlertComponent.new(state: :error, title: , extra_class_names: 'fr-mb-2w') do |c| = render Dsfr::AlertComponent.new(state: :error, title: , extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do - c.with_body do
- error_messages.each do |(error, path)| = render ExpandableErrorList.new(errors:)
%p.mt-2
= error.full_message
- if path.present?
= "(#{link_to 'corriger', path, class: 'fr-link'})"

View file

@ -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

View file

@ -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 <a href="%{href}">gestion de la démarche</a>

View file

@ -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))

View file

@ -1,9 +1,13 @@
class Procedure::RevisionChangesComponent < ApplicationComponent class Procedure::RevisionChangesComponent < ApplicationComponent
def initialize(changes:, previous_revision:) def initialize(new_revision:, previous_revision:)
@changes = changes
@previous_revision = previous_revision @previous_revision = previous_revision
@public_move_changes, @private_move_changes = changes.filter { _1.op == :move }.partition { !_1.private? } @new_revision = new_revision
@delete_champ_warning = !total_dossiers.zero? && !@changes.all?(&:can_rebase?)
@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 end
private private

View file

@ -80,3 +80,10 @@ fr:
update_expression_reguliere_exemple_text: Lexemple dexpression régulière de lannotation privée « %{label} » a été modifiée. Le nouvel exemple est « %{to} ». update_expression_reguliere_exemple_text: Lexemple dexpression régulière de lannotation privée « %{label} » a été modifiée. Le nouvel exemple est « %{to} ».
remove_expression_reguliere_error_message: Le message derreur de lexpression régulière de lannotation privée « %{label} » a été supprimé. remove_expression_reguliere_error_message: Le message derreur de lexpression régulière de lannotation privée « %{label} » a été supprimé.
update_expression_reguliere_error_message: Le message derreur de lexpression régulière de lannotation privée « %{label} » a été modifiée. Le nouveau message est « %{to} ». update_expression_reguliere_error_message: Le message derreur de lexpression régulière de lannotation privée « %{label} » a été modifiée. Le nouveau message est « %{to} ».
ineligibilite_rules:
add: La condition dinéligibilité « %{new_condition} » a été ajoutée.
remove: La condition dinéligibilité « %{previous_condition} » a été supprimée
update: La conditon dinéligibilité « %{previous_condition} » a été changée pour « %{new_condition} »
enabled: "Linéligibilité des dossiers a été activée"
disabled: "Linéligibilité des dossiers a été désactivée"
message_updated: "Le message dinéligibilité a été changé pour « %{ineligibilite_message} »"

View file

@ -2,7 +2,7 @@
- list.with_empty do - list.with_empty do
= t('.no_changes') = t('.no_changes')
- @changes.each do |change| - @tdc_changes.each do |change|
- prefix = change.private? ? 'private' : 'public' - prefix = change.private? ? 'private' : 'public'
- case change.op - case change.op
- when :add - when :add
@ -176,3 +176,7 @@
- list.with_item do - list.with_item do
.fr-alert.fr-alert--warning.fr-mt-1v .fr-alert.fr-alert--warning.fr-mt-1v
= t(".invalid_routing_rules_alert") = t(".invalid_routing_rules_alert")
- @ineligibilite_rules_changes.each do |change|
- list.with_item do
= t(".ineligibilite_rules.#{change.op}", **change.i18n_params)

View file

@ -10,7 +10,7 @@
.flex.justify-start.width-33 .flex.justify-start.width-33
.cell.flex.justify-start.column.flex-grow .cell.flex.justify-start.column.flex-grow
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ) = 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 .flex.column.justify-start.flex-grow
.cell .cell
@ -136,6 +136,10 @@
%span %span
utilisé pour utilisé pour
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules')) = 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('leligibilité des dossiers', edit_admin_procedure_ineligibilite_rules_path(revision.procedure_id))
- else - 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 = 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 %span.sr-only Supprimer

View file

@ -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

View file

@ -231,9 +231,9 @@ module Users
def submit_brouillon def submit_brouillon
@dossier = dossier_with_champs(pj_template: false) @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.passer_en_construction!
@dossier.process_declarative! @dossier.process_declarative!
@dossier.process_sva_svr! @dossier.process_sva_svr!
@ -278,9 +278,9 @@ module Users
editing_fork_origin.resolve_pending_correction editing_fork_origin.resolve_pending_correction
end 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.merge_fork(@dossier)
editing_fork_origin.submit_en_construction! editing_fork_origin.submit_en_construction!
@ -288,7 +288,6 @@ module Users
else else
respond_to do |format| respond_to do |format|
format.html do format.html do
@dossier = editing_fork_origin
render :modifier render :modifier
end end
@ -303,10 +302,10 @@ module Users
def update def update
@dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier @dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier
@dossier = dossier_with_champs(pj_template: false) @dossier = dossier_with_champs(pj_template: false)
@errors = update_dossier_and_compute_errors @can_passer_en_construction_was = @dossier.can_passer_en_construction?
update_dossier_and_compute_errors
@dossier.index_search_terms_later if @errors.empty? @dossier.index_search_terms_later if @dossier.errors.empty?
@can_passer_en_construction_is = @dossier.can_passer_en_construction?
respond_to do |format| respond_to do |format|
format.turbo_stream do format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?)) @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 def submit_dossier_and_compute_errors
@dossier.validate(:champs_public_value) @dossier.validate(:champs_public_value)
@dossier.check_mandatory_and_visible_champs
errors = @dossier.errors
@dossier.check_mandatory_and_visible_champs.each do |error_on_champ|
errors.import(error_on_champ)
end
if @dossier.editing_fork_origin&.pending_correction? if @dossier.editing_fork_origin&.pending_correction?
@dossier.editing_fork_origin.validate(:champs_public_value) @dossier.editing_fork_origin.validate(:champs_public_value)
@dossier.editing_fork_origin.errors.where(:pending_correction).each do |error| @dossier.editing_fork_origin.errors.where(:pending_correction).each do |error|
errors.import(error) @dossier.errors.import(error)
end end
end end
errors
end end
def ensure_ownership! def ensure_ownership!

View file

@ -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);
}
}

View file

@ -21,6 +21,10 @@ module ChampConditionalConcern
end end
end end
def reset_visible # recompute after a dossier update
remove_instance_variable :@visible if instance_variable_defined? :@visible
end
private private
def champs_for_condition def champs_for_condition

View file

@ -22,7 +22,7 @@ module DossierRebaseConcern
end end
def pending_changes 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 end
def can_rebase_mandatory_change?(stable_id) def can_rebase_mandatory_change?(stable_id)

View file

@ -156,7 +156,7 @@ class Dossier < ApplicationRecord
state :sans_suite state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction, after_commit: :after_commit_passer_en_construction do 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 end
event :passer_en_instruction, after: :after_passer_en_instruction, after_commit: :after_commit_passer_en_instruction do 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? procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
end 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? def can_passer_en_instruction?
return false if blocked_with_pending_correction? return false if blocked_with_pending_correction?
@ -936,6 +942,7 @@ class Dossier < ApplicationRecord
.map do |champ| .map do |champ|
champ.errors.add(:value, :missing) champ.errors.add(:value, :missing)
end end
.each { errors.import(_1) }
end end
def demander_un_avis!(avis) def demander_un_avis!(avis)

View file

@ -293,7 +293,7 @@ class Procedure < ApplicationRecord
validates_with MonAvisEmbedValidator validates_with MonAvisEmbedValidator
validates_associated :draft_revision, on: :publication validate :validates_associated_draft_revision_with_context
validates_associated :initiated_mail, on: :publication validates_associated :initiated_mail, on: :publication
validates_associated :received_mail, on: :publication validates_associated :received_mail, on: :publication
validates_associated :closed_mail, on: :publication validates_associated :closed_mail, on: :publication
@ -431,11 +431,15 @@ class Procedure < ApplicationRecord
def draft_changed? def draft_changed?
preload_draft_and_published_revisions 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 end
def revision_changes def types_de_champ_revision_changes
published_revision.compare(draft_revision) published_revision.compare_types_de_champ(draft_revision)
end
def ineligibilite_rules_revision_changes
published_revision.compare_ineligibilite_rules(draft_revision)
end end
def preload_draft_and_published_revisions def preload_draft_and_published_revisions
@ -1017,6 +1021,13 @@ class Procedure < ApplicationRecord
private 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 def validate_auto_archive_on_in_the_future
return if auto_archive_on.nil? return if auto_archive_on.nil?
return if auto_archive_on.future? return if auto_archive_on.future?

View file

@ -1,4 +1,5 @@
class ProcedureRevision < ApplicationRecord class ProcedureRevision < ApplicationRecord
include Logic
self.implicit_order_column = :created_at self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
@ -17,8 +18,19 @@ class ProcedureRevision < ApplicationRecord
scope :ordered, -> { order(:created_at) } scope :ordered, -> { order(:created_at) }
validates :ineligibilite_message, presence: true, if: -> { ineligibilite_enabled? }
delegate :path, to: :procedure, prefix: true 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 def build_champs_public
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc # 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) types_de_champ_public.reload.map(&:build_champ)
@ -136,16 +148,18 @@ class ProcedureRevision < ApplicationRecord
!draft? !draft?
end end
def different_from?(revision) def compare_types_de_champ(revision)
revision_types_de_champ != revision.revision_types_de_champ
end
def compare(revision)
changes = [] changes = []
changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ) changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ)
changes changes
end end
def compare_ineligibilite_rules(revision)
changes = []
changes += compare_revision_ineligibilite_rules(revision)
changes
end
def dossier_for_preview(user) def dossier_for_preview(user)
dossier = Dossier dossier = Dossier
.create_with(autorisation_donnees: true) .create_with(autorisation_donnees: true)
@ -251,6 +265,10 @@ class ProcedureRevision < ApplicationRecord
types_de_champ_public.filter(&:routable?) types_de_champ_public.filter(&:routable?)
end end
def conditionable_types_de_champ
types_de_champ_for(scope: :public).filter(&:conditionable?)
end
private private
def compute_estimated_fill_duration def compute_estimated_fill_duration
@ -318,6 +336,29 @@ class ProcedureRevision < ApplicationRecord
end end
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) def compare_type_de_champ(from_type_de_champ, to_type_de_champ, from_coordinates, to_coordinates)
changes = [] changes = []
if from_type_de_champ.type_champ != to_type_de_champ.type_champ if from_type_de_champ.type_champ != to_type_de_champ.type_champ
@ -442,6 +483,13 @@ class ProcedureRevision < ApplicationRecord
changes changes
end 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) def replace_type_de_champ_by_clone(coordinate)
cloned_type_de_champ = coordinate.type_de_champ.deep_clone do |original, kopy| cloned_type_de_champ = coordinate.type_de_champ.deep_clone do |original, kopy|
ClonePiecesJustificativesService.clone_attachments(original, kopy) ClonePiecesJustificativesService.clone_attachments(original, kopy)

View file

@ -1,17 +1,19 @@
class ProcedureRevisionChange class ProcedureRevisionChange
attr_reader :type_de_champ class TypeDeChange
def initialize(type_de_champ) attr_reader :type_de_champ
@type_de_champ = 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 end
def label = @type_de_champ.libelle class AddChamp < TypeDeChange
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
def initialize(type_de_champ) def initialize(type_de_champ)
super(type_de_champ) super(type_de_champ)
end end
@ -23,7 +25,7 @@ class ProcedureRevisionChange
def to_h = super.merge(mandatory: mandatory?) def to_h = super.merge(mandatory: mandatory?)
end end
class RemoveChamp < ProcedureRevisionChange class RemoveChamp < TypeDeChange
def initialize(type_de_champ) def initialize(type_de_champ)
super(type_de_champ) super(type_de_champ)
end end
@ -32,7 +34,7 @@ class ProcedureRevisionChange
def can_rebase?(dossier = nil) = true def can_rebase?(dossier = nil) = true
end end
class MoveChamp < ProcedureRevisionChange class MoveChamp < TypeDeChange
attr_reader :from, :to attr_reader :from, :to
def initialize(type_de_champ, from, to) def initialize(type_de_champ, from, to)
@ -46,7 +48,7 @@ class ProcedureRevisionChange
def to_h = super.merge(from:, to:) def to_h = super.merge(from:, to:)
end end
class UpdateChamp < ProcedureRevisionChange class UpdateChamp < TypeDeChange
attr_reader :attribute, :from, :to attr_reader :attribute, :from, :to
def initialize(type_de_champ, attribute, from, to) def initialize(type_de_champ, attribute, from, to)
@ -75,4 +77,48 @@ class ProcedureRevisionChange
end end
end 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 end

View file

@ -75,4 +75,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
def used_by_routing_rules? def used_by_routing_rules?
stable_id.in?(procedure.stable_ids_used_by_routing_rules) stable_id.in?(procedure.stable_ids_used_by_routing_rules)
end end
def used_by_ineligibilite_rules?
revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
end
end end

View file

@ -657,6 +657,10 @@ class TypeDeChamp < ApplicationRecord
type_champ.in?(ROUTABLE_TYPES) type_champ.in?(ROUTABLE_TYPES)
end end
def conditionable?
Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(type_champ)
end
def invalid_regexp? def invalid_regexp?
self.errors.delete(:expression_reguliere) self.errors.delete(:expression_reguliere)
self.errors.delete(:expression_reguliere_exemple_text) self.errors.delete(:expression_reguliere_exemple_text)

View file

@ -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)

View file

@ -0,0 +1 @@
= render partial: 'update'

View file

@ -0,0 +1 @@
= render partial: 'update'

View file

@ -0,0 +1 @@
= render partial: 'update'

View file

@ -0,0 +1 @@
= render partial: 'update'

View file

@ -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 dinéligibilité ne pourront pas être déposés. Plus dinformations sur linéligibilité des dossiers dans la
= link_to('doc', ELIGIBILITE_URL, title: "Document sur linéligibilité des dossiers", **external_link_attributes)
- if !@procedure.draft_revision.conditionable_types_de_champ.present?
%p.fr-mt-2w.fr-mb-2w
Pour configurer linéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions diné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 diné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)

View file

@ -0,0 +1 @@
= render partial: 'update'

View file

@ -8,7 +8,7 @@
%p.mb-2= t('.draft_changed_procedure_alert') %p.mb-2= t('.draft_changed_procedure_alert')
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c| = render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do - 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? - if procedure.close?
= render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f } = render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f }
- elsif @procedure.brouillon? && @procedure.missing_steps.empty? - elsif @procedure.brouillon? && @procedure.missing_steps.empty?

View file

@ -13,7 +13,6 @@
- previous_revision = nil - previous_revision = nil
- @procedure.revisions.each do |revision| - @procedure.revisions.each do |revision|
- if previous_revision.present? && !revision.draft? - if previous_revision.present? && !revision.draft?
- changes = previous_revision.compare(revision)
- dossiers = revision.dossiers.visible_by_administration - dossiers = revision.dossiers.visible_by_administration
- dossiers_en_construction_count = dossiers.state_en_construction.count - dossiers_en_construction_count = dossiers.state_en_construction.count
- dossiers_en_instruction_count = dossiers.state_en_instruction.count - dossiers_en_instruction_count = dossiers.state_en_instruction.count
@ -31,7 +30,7 @@
%p= t('.dossiers_en_construction', count: dossiers_en_construction_count) %p= t('.dossiers_en_construction', count: dossiers_en_construction_count)
- elsif !dossiers_en_instruction_count.zero? - elsif !dossiers_en_instruction_count.zero?
%p= t('.dossiers_en_instruction', count: dossiers_en_instruction_count) %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 - previous_revision = revision
= render Procedure::FixedFooterComponent.new(procedure: @procedure) = render Procedure::FixedFooterComponent.new(procedure: @procedure)

View file

@ -30,8 +30,8 @@
- if @procedure.draft_changed? - if @procedure.draft_changed?
= render Dsfr::CalloutComponent.new(title: t(:has_changes, scope: [:administrateurs, :revision_changes]), icon: "fr-fi-information-line") do |c| = render Dsfr::CalloutComponent.new(title: t(:has_changes, scope: [:administrateurs, :revision_changes]), icon: "fr-fi-information-line") do |c|
- c.with_body do - 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::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 - c.with_bottom do
%ul.fr-mt-2w.fr-btns-group.fr-btns-group--inline %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::PresentationComponent.new(procedure: @procedure)
= render Procedure::Card::ZonesComponent.new(procedure: @procedure) if Rails.application.config.ds_zonage_enabled = render Procedure::Card::ZonesComponent.new(procedure: @procedure) if Rails.application.config.ds_zonage_enabled
= render Procedure::Card::ChampsComponent.new(procedure: @procedure) = 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::ServiceComponent.new(procedure: @procedure, administrateur: current_administrateur)
= render Procedure::Card::AdministrateursComponent.new(procedure: @procedure) = render Procedure::Card::AdministrateursComponent.new(procedure: @procedure)
= render Procedure::Card::InstructeursComponent.new(procedure: @procedure) = render Procedure::Card::InstructeursComponent.new(procedure: @procedure)

View file

@ -10,7 +10,7 @@
= render NestedForms::FormOwnerComponent.new = 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| = 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 %header.mb-6
.fr-highlight .fr-highlight
%p.fr-text--sm %p.fr-text--sm
@ -25,4 +25,6 @@
= render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier) = render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier)
= render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier)
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false) = render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)

View file

@ -1 +1,7 @@
= render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier } = 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))

View file

@ -61,6 +61,9 @@ DS_ENV="staging"
# Instance customization: URL of the Routage documentation # Instance customization: URL of the Routage documentation
# ROUTAGE_URL="" # ROUTAGE_URL=""
# #
# Instance customization: URL of the EligibiliteDossier documentation
# ELIGIBILITE_URL=""
#
# Instance customization: URL of the accessibility statement # Instance customization: URL of the accessibility statement
# ACCESSIBILITE_URL="" # ACCESSIBILITE_URL=""

View file

@ -37,6 +37,7 @@ CGU_URL = ENV.fetch("CGU_URL", [DOC_URL, "cgu"].join("/"))
MENTIONS_LEGALES_URL = ENV.fetch("MENTIONS_LEGALES_URL", "/mentions-legales") MENTIONS_LEGALES_URL = ENV.fetch("MENTIONS_LEGALES_URL", "/mentions-legales")
ACCESSIBILITE_URL = ENV.fetch("ACCESSIBILITE_URL", "/declaration-accessibilite") ACCESSIBILITE_URL = ENV.fetch("ACCESSIBILITE_URL", "/declaration-accessibilite")
ROUTAGE_URL = ENV.fetch("ROUTAGE_URL", [DOC_URL, "/pour-aller-plus-loin/routage"].join("/")) 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("/") API_DOC_URL = [DOC_URL, "api-graphql"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].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("/") WEBHOOK_ALTERNATIVE_DOC_URL = [DOC_URL, "api-graphql", "cas-dusages-exemple-dimplementation", "synchroniser-les-dossiers-modifies-sur-ma-demarche"].join("/")

View file

@ -606,6 +606,7 @@ en:
otp_attempt: 'OTP code (only if you have already activated 2FA)' otp_attempt: 'OTP code (only if you have already activated 2FA)'
procedure: procedure:
zone: This procedure is run by zone: This procedure is run by
ineligibilite_rules: "Eligibility rules"
champs: champs:
value: Value value: Value
default_mail_attributes: &default_mail_attributes default_mail_attributes: &default_mail_attributes
@ -667,6 +668,10 @@ en:
path: path:
taken: is already used for procedure. You cannot use it because it belongs to another administrator. 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 '-'. 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": "dossier/champs":
format: "%{message}" format: "%{message}"
attributes: attributes:

View file

@ -610,6 +610,7 @@ fr:
otp_attempt: 'Code OTP (uniquement si vous avez déjà activé 2FA)' otp_attempt: 'Code OTP (uniquement si vous avez déjà activé 2FA)'
procedure: procedure:
zone: La démarche est mise en œuvre par zone: La démarche est mise en œuvre par
ineligibilite_rules: "Les règles dinéligibilité"
champs: champs:
value: Valeur du champ value: Valeur du champ
default_mail_attributes: &default_mail_attributes default_mail_attributes: &default_mail_attributes
@ -669,6 +670,10 @@ fr:
path: path:
taken: est déjà utilisé par une démarche. Vous ne pouvez pas lutiliser car il appartient à un autre administrateur. taken: est déjà utilisé par une démarche. Vous ne pouvez pas lutiliser car il appartient à un autre administrateur.
invalid: nest 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. invalid: nest 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": "dossier/champs":
format: "%{message}" format: "%{message}"
attributes: attributes:

View file

@ -72,16 +72,16 @@ en:
invalid: 'invalid format' invalid: 'invalid format'
draft_types_de_champ_public: draft_types_de_champ_public:
format: 'Public field %{message}' format: 'Public field %{message}'
invalid_condition: "« %{value} » have an invalid logic" invalid_condition: "have an invalid logic"
empty_repetition: '« %{value} » requires at least one field' empty_repetition: 'requires at least one field'
empty_drop_down: '« %{value} » requires at least one option' empty_drop_down: 'requires at least one option'
inconsistent_header_section: "« %{value} » %{custom_message}" inconsistent_header_section: "%{custom_message}"
draft_types_de_champ_private: draft_types_de_champ_private:
format: 'Private field %{message}' format: 'Private field %{message}'
invalid_condition: "« %{value} » have an invalid logic" invalid_condition: "have an invalid logic"
empty_repetition: '« %{value} » requires at least one field' empty_repetition: 'requires at least one field'
empty_drop_down: '« %{value} » requires at least one option' empty_drop_down: 'requires at least one option'
inconsistent_header_section: "« %{value} » %{custom_message}" inconsistent_header_section: "%{custom_message}"
attestation_template: attestation_template:
format: "%{attribute} %{message}" format: "%{attribute} %{message}"
initiated_mail: initiated_mail:

View file

@ -8,7 +8,7 @@ fr:
procedure: procedure:
hints: hints:
description: Décrivez en quelques lignes le contexte, la finalité, etc. 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é sil 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é sil y en a, les pré-requis, etc.
description_pj: Décrivez la liste des pièces jointes à fournir sil y en a description_pj: Décrivez la liste des pièces jointes à fournir sil 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" 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/'" cadre_juridique: "Exemple: 'https://www.legifrance.gouv.fr/'"
@ -78,16 +78,16 @@ fr:
invalid: 'na pas le bon format' invalid: 'na pas le bon format'
draft_types_de_champ_public: draft_types_de_champ_public:
format: 'Le champ %{message}' format: 'Le champ %{message}'
invalid_condition: "« %{value} » a une logique conditionnelle invalide" invalid_condition: "a une logique conditionnelle invalide"
empty_repetition: '« %{value} » doit comporter au moins un champ répétable' empty_repetition: 'doit comporter au moins un champ répétable'
empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable' empty_drop_down: 'doit comporter au moins un choix sélectionnable'
inconsistent_header_section: "« %{value} » %{custom_message}" inconsistent_header_section: "%{custom_message}"
draft_types_de_champ_private: draft_types_de_champ_private:
format: 'Lannotation privée %{message}' format: 'Lannotation privée %{message}'
invalid_condition: "« %{value} » a une logique conditionnelle invalide" invalid_condition: "a une logique conditionnelle invalide"
empty_repetition: '« %{value} » doit comporter au moins un champ répétable' empty_repetition: 'doit comporter au moins un champ répétable'
empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable' empty_drop_down: 'doit comporter au moins un choix sélectionnable'
inconsistent_header_section: "« %{value} » %{custom_message}" inconsistent_header_section: "%{custom_message}"
attestation_template: attestation_template:
format: "%{attribute} %{message}" format: "%{attribute} %{message}"
initiated_mail: initiated_mail:

View file

@ -0,0 +1,7 @@
fr:
activerecord:
attributes:
procedure_revision:
ineligibilite_message: Message dinéligibilité
hints:
ineligibilite_message: "Ce message sera affiché à lusager si son dossier est bloqué et lui expliquera la raison de son inéligibilité."

View file

@ -608,6 +608,14 @@ Rails.application.routes.draw do
delete :delete_row, on: :member delete :delete_row, on: :member
end 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 patch :update_defaut_groupe_instructeur, controller: 'routing_rules', as: :update_defaut_groupe_instructeur
put 'clone' put 'clone'

View file

@ -0,0 +1,5 @@
class AddTransitionsRulesToProcedureRevisions < ActiveRecord::Migration[7.0]
def change
add_column :procedure_revisions, :ineligibilite_rules, :jsonb
end
end

View file

@ -0,0 +1,5 @@
class AddDossierIneligbleMessageToProcedureRevisions < ActiveRecord::Migration[7.0]
def change
add_column :procedure_revisions, :ineligibilite_message, :text
end
end

View file

@ -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

View file

@ -863,6 +863,9 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_27_090508) do
create_table "procedure_revisions", force: :cascade do |t| create_table "procedure_revisions", force: :cascade do |t|
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", precision: nil, null: false
t.bigint "dossier_submitted_message_id" 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.bigint "procedure_id", null: false
t.datetime "published_at", precision: nil t.datetime "published_at", precision: nil
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -11,27 +11,33 @@ describe Procedure::ErrorsSummary, type: :component do
context 'when :publication' do context 'when :publication' do
let(:validation_context) { :publication } let(:validation_context) { :publication }
it 'shows errors for public and private tdc' do it 'shows errors and links 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_content("Erreur : Des problèmes empêchent la publication de la démarche")
expect(page).to have_text("Lannotation privée « private » doit comporter au moins un choix sélectionnable") 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
end end
context 'when :types_de_champ_public_editor' do context 'when :types_de_champ_public_editor' do
let(:validation_context) { :types_de_champ_public_editor } let(:validation_context) { :types_de_champ_public_editor }
it 'shows errors for public only tdc' do it 'shows errors and links for public only tdc' do
expect(page).to have_text("Le champ « public » doit comporter au moins un choix sélectionnable") expect(page).to have_text("Erreur : Les champs formulaire contiennent des erreurs")
expect(page).not_to have_text("Lannotation privée « private » doit comporter au moins un choix sélectionnable") 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
end end
context 'when :types_de_champ_private_editor' do context 'when :types_de_champ_private_editor' do
let(:validation_context) { :types_de_champ_private_editor } let(:validation_context) { :types_de_champ_private_editor }
it 'shows errors for private only tdc' do it 'shows errors and links 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("Erreur : Les annotations privées contiennent des erreurs")
expect(page).to have_text("Lannotation privée « private » doit comporter au moins un choix sélectionnable") 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 end
end end
@ -52,16 +58,24 @@ describe Procedure::ErrorsSummary, type: :component do
before { subject } before { subject }
it 'renders all errors on champ' do it 'renders all errors and links 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_selector("a", text: "drop down list requires options")
expect(page).to have_text("Le champ « repetition requires children » doit comporter au moins un champ répétable") expect(page).to have_content("doit comporter au moins un choix sélectionnable")
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") expect(page).to have_selector("a", text: "repetition requires children")
# TODO, test attestation_template, initiated_mail, :received_mail, :closed_mail, :refused_mail, :without_continuation_mail, :re_instructed_mail 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
end end
describe 'render error for other kind of associated objects' do describe 'render error for other kind of associated objects' do
include Logic
let(:validation_context) { :publication } let(:validation_context) { :publication }
let(:procedure) { create(:procedure, attestation_template:, initiated_mail:) } let(:procedure) { create(:procedure, attestation_template:, initiated_mail:) }
let(:attestation_template) { build(:attestation_template) } let(:attestation_template) { build(:attestation_template) }
@ -69,12 +83,15 @@ describe Procedure::ErrorsSummary, type: :component do
before do before do
[:attestation_template, :initiated_mail].map { procedure.send(_1).update_column(:body, '--invalidtag--') } [: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 subject
end end
it 'render error nicely' do it 'render error nicely' do
expect(page).to have_text("Le modèle dattestation n'est pas valide") expect(page).to have_selector("a", text: "Les règles dinéligibilité")
expect(page).to have_text("Lemail de notification de passage de dossier en instruction n'est pas valide") expect(page).to have_selector("a", text: "Le modèle dattestation")
expect(page).to have_selector("a", text: "Lemail de notification de passage de dossier en instruction")
expect(page).to have_text("n'est pas valide", count: 2)
end end
end end
end end

View file

@ -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

View file

@ -2,10 +2,12 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
describe 'render' do describe 'render' do
let(:component) { described_class.new(coordinate:, upper_coordinates: []) } let(:component) { described_class.new(coordinate:, upper_coordinates: []) }
let(:routing_rules_stable_ids) { [] } let(:routing_rules_stable_ids) { [] }
let(:ineligibilite_rules_used?) { false }
before do before do
Flipper.enable_actor(:engagement_juridique_type_de_champ, procedure) 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(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) render_inline(component)
end end
@ -29,6 +31,15 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
expect(page).to have_text(/utilisé pour\nle routage/) expect(page).to have_text(/utilisé pour\nle routage/)
end end
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(/leligibilité des dossiers/)
end
end
end end
describe 'tdc ej' do describe 'tdc ej' do

View file

@ -10,16 +10,18 @@ describe TypesDeChampEditor::EditorComponent, type: :component do
context 'types_de_champ_public' do context 'types_de_champ_public' do
let(:is_annotation) { false } let(:is_annotation) { false }
it 'does not render private champs errors' do it 'does not render private champs errors' do
expect(subject).not_to have_text("« private » doit comporter au moins un choix sélectionnable") expect(subject).not_to have_text("private")
expect(subject).to have_text("« public » doit comporter au moins un choix sélectionnable") expect(subject).to have_selector("a", text: "public")
expect(subject).to have_text("doit comporter au moins un choix sélectionnable")
end end
end end
context 'types_de_champ_private' do context 'types_de_champ_private' do
let(:is_annotation) { true } let(:is_annotation) { true }
it 'does not render public champs errors' do it 'does not render public champs errors' do
expect(subject).to have_text("« private » doit comporter au moins un choix sélectionnable") expect(subject).to have_selector("a", text: "private")
expect(subject).not_to have_text("« public » doit comporter au moins un choix sélectionnable") expect(subject).to have_text("doit comporter au moins un choix sélectionnable")
expect(subject).not_to have_text("public")
end end
end end
end end

View file

@ -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 diné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 diné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

View file

@ -398,7 +398,9 @@ describe Users::DossiersController, type: :controller do
describe '#submit_brouillon' do describe '#submit_brouillon' do
before { sign_in(user) } 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(: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(: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' } let(:value) { 'beautiful value' }
@ -439,9 +441,9 @@ describe Users::DossiersController, type: :controller do
render_views render_views
let(:error_message) { 'nop' } let(:error_message) { 'nop' }
before do before do
expect_any_instance_of(Dossier).to receive(:validate).and_return(false) allow_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(:errors).and_return(
[double(inner_error: double(base: first_champ), message: 'nop')] [instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')]
) )
subject subject
end end
@ -461,11 +463,8 @@ describe Users::DossiersController, type: :controller do
render_views render_views
let(:value) { nil } let(:value) { nil }
let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] }
before do before { subject }
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
subject
end
it { expect(response).to render_template(:brouillon) } it { expect(response).to render_template(:brouillon) }
it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } 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 render_views
before do before do
expect_any_instance_of(Dossier).to receive(:validate).and_return(false) allow_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(:errors).and_return(
[double(inner_error: double(base: first_champ), message: 'nop')] [double(inner_error: double(base: first_champ), message: 'nop')]
) )
@ -661,7 +660,8 @@ describe Users::DossiersController, type: :controller do
describe '#update brouillon' do describe '#update brouillon' do
before { sign_in(user) } 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(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first } let(:first_champ) { dossier.champs_public.first }
let(:piece_justificative_champ) { dossier.champs_public.last } let(:piece_justificative_champ) { dossier.champs_public.last }
@ -754,13 +754,66 @@ describe Users::DossiersController, type: :controller do
end end
end end
it "debounce search terms indexation" do context 'having ineligibilite_rules setup' do
# dossier creation trigger a first indexation and flag, include Logic
# so we we have to remove this flag render_views
dossier.debounce_index_search_terms_flag.remove
assert_enqueued_jobs(1, only: DossierIndexSearchTermsJob) do let(:types_de_champ_public) { [{ type: :text }, { type: :integer_number }] }
3.times { patch :update, params: payload, format: :turbo_stream } 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 end
end end
@ -868,8 +921,8 @@ describe Users::DossiersController, type: :controller do
context 'classic error' do context 'classic error' do
before do before do
expect_any_instance_of(Dossier).to receive(:save).and_return(false) allow_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(:errors).and_return(
[message: 'nop', inner_error: double(base: first_champ)] [message: 'nop', inner_error: double(base: first_champ)]
) )
subject subject

View file

@ -43,6 +43,8 @@ end
describe Logic::GreaterThanEq do describe Logic::GreaterThanEq do
include Logic include Logic
let(:champ) { create(:champ_integer_number, value: nil) }
it 'computes' do it 'computes' do
expect(greater_than_eq(constant(0), constant(1)).compute).to be(false) expect(greater_than_eq(constant(0), constant(1)).compute).to be(false)
expect(greater_than_eq(constant(1), constant(1)).compute).to be(true) expect(greater_than_eq(constant(1), constant(1)).compute).to be(true)

View file

@ -347,306 +347,417 @@ describe ProcedureRevision do
end end
end end
describe '#compare' do describe '#compare_types_de_champ' do
include Logic 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 } 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 context 'with a procedure with 2 tdcs' do
let(:procedure) do let(:procedure) do
create(:procedure, types_de_champ_public: [ create(:procedure, types_de_champ_public: [
{ type: :integer_number, libelle: 'l1' }, { type: :integer_number, libelle: 'l1' },
{ type: :text, libelle: 'l2' } { 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 end
context 'when a condition is added' do context 'when a type de champ is added' do
before do let(:procedure) { create(:procedure) }
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id) let(:new_tdc) do
second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3))) new_draft.add_type_de_champ(
type_champ: TypeDeChamp.type_champs.fetch(:text),
libelle: "Un champ text"
)
end end
before { new_tdc }
it do it do
is_expected.to eq([ is_expected.to eq([
{ {
attribute: :condition, op: :add,
from: nil, label: "Un champ text",
label: "l2",
op: :update,
private: false, private: false,
stable_id: second_tdc.stable_id, mandatory: false,
to: "(l1 == 3)" stable_id: new_tdc.stable_id
} }
]) ])
end end
end end
context 'when a condition is removed' do context 'when a type de champ is changed' do
before do context 'when libelle, description, and mandatory are changed' do
second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2))) let(:procedure) { create(:procedure, :with_type_de_champ) }
draft.reload
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id) before do
second.update(condition: nil) 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 end
it do it do
is_expected.to eq([ is_expected.to eq([
{ {
attribute: :condition, op: :move,
from: "(l1 == 2)", label: new_draft_third_tdc.libelle,
label: "l2",
op: :update,
private: false, private: false,
stable_id: second_tdc.stable_id, from: 2,
to: nil to: 1,
} stable_id: new_draft_third_tdc.stable_id
]) },
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, op: :move,
from: "(l1 == 2)", label: new_draft_second_tdc.libelle,
label: "l2",
op: :update,
private: false, private: false,
stable_id: second_tdc.stable_id, from: 1,
to: "(l1 == 3)" to: 2,
stable_id: new_draft_second_tdc.stable_id
} }
]) ])
end end
end end
end
context 'when a type de champ is added' do context 'when a type de champ is removed' 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
let(:procedure) { create(:procedure, :with_type_de_champ) } let(:procedure) { create(:procedure, :with_type_de_champ) }
before do before do
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id) new_draft.remove_type_de_champ(first_tdc.stable_id)
updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
end end
it do it do
is_expected.to eq([ is_expected.to eq([
{ {
op: :update, op: :remove,
attribute: :libelle,
label: first_tdc.libelle, label: first_tdc.libelle,
private: false, 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 stable_id: first_tdc.stable_id
} }
]) ])
end end
end end
context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do context 'when a child type de champ is transformed into a drop_down_list' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) } let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
before do before do
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id) 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'])
updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
end end
it do it do
is_expected.to eq([ is_expected.to eq([
{ {
op: :update, op: :update,
attribute: :collapsible_explanation_enabled, attribute: :type_champ,
label: first_tdc.libelle, label: "sub type de champ",
private: first_tdc.private?, private: false,
from: false, from: "text",
to: true, to: "drop_down_list",
stable_id: first_tdc.stable_id stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
}, },
{ {
op: :update, op: :update,
attribute: :collapsible_explanation_text, attribute: :drop_down_options,
label: first_tdc.libelle, label: "sub type de champ",
private: first_tdc.private?, private: false,
from: nil, from: [],
to: 'afficher au clique', to: ["one", "two"],
stable_id: first_tdc.stable_id 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
end end
end
context 'when a type de champ is moved' do describe 'compare_ineligibilite_rules' do
let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) } include Logic
let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second } let(:new_draft) { procedure.create_new_revision }
let(:new_draft_third_tdc) { new_draft.types_de_champ_public.third } subject { procedure.active_revision.compare_ineligibilite_rules(new_draft.reload) }
before do context 'when ineligibilite_rules changes' do
new_draft_second_tdc let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
new_draft_third_tdc let(:types_de_champ_public) { [{ type: :yes_no }] }
new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2) let(:yes_no_tdc) { new_draft.types_de_champ_public.first }
context 'when nothing changed' do
it { is_expected.to be_empty }
end end
it do context 'when ineligibilite_rules added' do
is_expected.to eq([ before do
{ new_draft.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
op: :move, end
label: new_draft_third_tdc.libelle,
private: false, it { is_expected.to include(an_instance_of(ProcedureRevisionChange::AddEligibiliteRuleChange)) }
from: 2, end
to: 1,
stable_id: new_draft_third_tdc.stable_id 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)))
op: :move, end
label: new_draft_second_tdc.libelle,
private: false, it { is_expected.to include(an_instance_of(ProcedureRevisionChange::RemoveEligibiliteRuleChange)) }
from: 1, end
to: 2,
stable_id: new_draft_second_tdc.stable_id 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 end
end
context 'when a type de champ is removed' do describe 'ineligibilite_rules_are_valid?' do
let(:procedure) { create(:procedure, :with_type_de_champ) } include Logic
let(:procedure) { create(:procedure) }
before do let(:draft_revision) { procedure.draft_revision }
new_draft.remove_type_de_champ(first_tdc.stable_id) let(:ineligibilite_message) { 'ok' }
end let(:ineligibilite_enabled) { true }
before do
it do procedure.draft_revision.update(ineligibilite_rules:, ineligibilite_message:, ineligibilite_enabled:)
is_expected.to eq([
{
op: :remove,
label: first_tdc.libelle,
private: false,
stable_id: first_tdc.stable_id
}
])
end
end end
context 'when a child type de champ is transformed into a drop_down_list' do context 'when ineligibilite_rules are valid' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) } let(:ineligibilite_rules) { ds_eq(constant(true), constant(true)) }
it 'is valid' do
before do expect(draft_revision.validate(:publication)).to be_truthy
child = new_draft.children_of(new_draft.types_de_champ_public.last).first expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_truthy
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
}
])
end end
end end
context 'when ineligibilite_rules are invalid on simple champ' do
context 'when a child type de champ is transformed into a map' do let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) } it 'is invalid' do
expect(draft_revision.validate(:publication)).to be_falsey
before do expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
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 end
end
it do context 'when ineligibilite_rules are invalid on repetition champ' do
is_expected.to eq([ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
{ let(:procedure) { create(:procedure, types_de_champ_public:) }
op: :update, let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :integer_number }] }] }
attribute: :type_champ, let(:tdc_number) { draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'integer_number' } }
label: "sub type de champ", let(:ineligibilite_rules) do
private: false, ds_eq(champ_value(tdc_number.stable_id), constant(true))
from: "text", end
to: "carte", it 'is invalid' do
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id expect(draft_revision.validate(:publication)).to be_falsey
}, expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
{
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
end end

View file

@ -372,12 +372,12 @@ describe Procedure do
] ]
end end
let(:types_de_champ_private) { [] } let(:types_de_champ_private) { [] }
let(:invalid_repetition_error_message) { 'Le champ « Enfants » doit comporter au moins un champ répétable' } let(:invalid_repetition_error_message) { "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_drop_down_error_message) { "doit comporter au moins un choix sélectionnable" }
it 'validates that no repetition type de champ is empty' do it 'validates that no repetition type de champ is empty' do
procedure.validate(:publication) procedure.validate(:publication)
expect(procedure.errors.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 new_draft = procedure.draft_revision
repetition = procedure.draft_revision.types_de_champ_public.find(&:repetition?) 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) new_draft.revision_types_de_champ.create(type_de_champ: create(:type_de_champ), position: 0, parent: parent_coordinate)
procedure.validate(:publication) 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 end
it 'validates that no drop-down type de champ is empty' do it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication) 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 = procedure.draft_revision.types_de_champ_public.find(&:drop_down_list?)
drop_down.update!(drop_down_list_value: "--title--\r\nsome value") drop_down.update!(drop_down_list_value: "--title--\r\nsome value")
procedure.reload.validate(:publication) 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
end end
@ -408,17 +408,21 @@ describe Procedure do
end end
let(:types_de_champ_public) { [] } let(:types_de_champ_public) { [] }
let(:invalid_repetition_error_message) { 'Lannotation privée « Enfants » doit comporter au moins un champ répétable' } let(:invalid_repetition_error_message) { "doit comporter au moins un champ répétable" }
let(:invalid_drop_down_error_message) { 'Lannotation privée « Civilité » doit comporter au moins un choix sélectionnable' } 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 it 'validates that no repetition type de champ is empty' do
procedure.validate(:publication) 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 end
it 'validates that no drop-down type de champ is empty' do it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication) 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
end end
@ -441,7 +445,7 @@ describe Procedure do
include Logic 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_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(: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 it 'validate without context' do
procedure.validate procedure.validate

View file

@ -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 linéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions dinéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire : ")
click_on "Ajouter un champ supportant les conditions diné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 dinéligibilité", with: "vous n'etes pas eligible"
find('label', text: 'Bloquer le dépôt des dossiers répondant à des conditions dinéligibilité').click
click_on "Ajouter une règle diné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

View file

@ -72,8 +72,8 @@ describe 'Publishing a procedure', js: true do
visit admin_procedure_path(procedure) 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('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("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("Civilité doit comporter au moins un choix sélectionnable")
visit admin_procedure_publication_path(procedure) visit admin_procedure_publication_path(procedure)
expect(find_field('procedure_path').value).to eq procedure.path 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 scenario 'an error message prevents the publication' do
visit admin_procedure_path(procedure) visit admin_procedure_path(procedure)
expect(page).to have_content('Des problèmes empêchent la publication des modifications') 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) expect(page).to have_button('Publier les modifications', disabled: true)
end end
end end

View file

@ -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

View file

@ -149,4 +149,17 @@ describe 'shared/dossiers/edit', type: :view do
end end
end 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 end