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:
commit
f6a5e932b8
84 changed files with 1772 additions and 387 deletions
|
@ -57,5 +57,14 @@ form.form > .conditionnel {
|
|||
select.alert {
|
||||
border-color: $dark-red;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class Conditions::ConditionsComponent < ApplicationComponent
|
|||
|
||||
def available_targets_for_select
|
||||
@source_tdcs
|
||||
.filter { |tdc| ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(tdc.type_champ) }
|
||||
.filter(&:conditionable?)
|
||||
.map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
|
||||
end
|
||||
|
||||
|
|
34
app/components/conditions/ineligibilite_rules_component.rb
Normal file
34
app/components/conditions/ineligibilite_rules_component.rb
Normal 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
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
fr:
|
||||
display_if: Bloquer si
|
||||
select: Sélectionner
|
||||
add_condition: Ajouter une règle d’inéligibilité
|
||||
remove_a_row: Supprimer une règle
|
|
@ -0,0 +1,49 @@
|
|||
%div{ id: dom_id(@draft_revision, :ineligibilite_rules) }
|
||||
= render Procedure::PendingRepublishComponent.new(procedure: @draft_revision.procedure, render_if: pending_changes?)
|
||||
= render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs)
|
||||
.fr-fieldset
|
||||
= form_for(@draft_revision, url: change_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), html: { id: 'ineligibilite_form', class: 'width-100' }) do |f|
|
||||
.fr-fieldset__element
|
||||
.fr-toggle.fr-toggle--label-left
|
||||
= f.check_box :ineligibilite_enabled, class: 'fr-toggle__input', data: @opt
|
||||
= f.label :ineligibilite_enabled, "Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité", data: { 'fr-checked-label': "Activé", 'fr-unchecked-label': "Désactivé" }, class: 'fr-toggle__label'
|
||||
|
||||
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :ineligibilite_message, input_type: :text_area, opts: {rows: 5})
|
||||
|
||||
.fr-mx-1w.fr-label.fr-py-0.fr-mb-1w.fr-mt-2w
|
||||
Conditions d’inéligibilité
|
||||
%span.fr-hint-text Vous pouvez utiliser une ou plusieurs condtions pour bloquer le dépot.
|
||||
.fr-fieldset__element
|
||||
= form_tag admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), method: :patch, data: { turbo: true, controller: 'autosave' }, class: 'form width-100' do
|
||||
.conditionnel.width-100
|
||||
%table.condition-table
|
||||
- if rows.size > 0
|
||||
%thead
|
||||
%tr
|
||||
%th.fr-pt-0.far-left
|
||||
%th.fr-pt-0.target Champ Cible
|
||||
%th.fr-pt-0.operator Opérateur
|
||||
%th.fr-pt-0.value Valeur
|
||||
%th.fr-pt-0.delete-column
|
||||
%tbody
|
||||
- rows.each.with_index do |(targeted_champ, operator_name, value), row_index|
|
||||
%tr
|
||||
%td.far-left= far_left_tag(row_index)
|
||||
%td.target= left_operand_tag(targeted_champ, row_index)
|
||||
%td.operator= operator_tag(operator_name, targeted_champ, row_index)
|
||||
%td.value= right_operand_tag(targeted_champ, value, row_index, operator_name)
|
||||
%td.delete-column= delete_condition_tag(row_index)
|
||||
%tfoot
|
||||
%tr
|
||||
%td.text-right{ colspan: 5 }= add_condition_tag
|
||||
|
||||
.padded-fixed-footer
|
||||
.fixed-footer
|
||||
.fr-container
|
||||
.fr-grid-row.fr-col-offset-md-2.fr-col-md-8
|
||||
.fr-col-12
|
||||
%ul.fr-btns-group.fr-btns-group--inline-md
|
||||
%li
|
||||
= link_to "Annuler et revenir à l'écran de gestion", admin_procedure_path(id: @draft_revision.procedure), class: 'fr-btn fr-btn--secondary', data: { confirm: 'Si vous avez fait des modifications elles ne seront pas sauvegardées.'}
|
||||
%li
|
||||
= button_tag "Enregistrer", class: "fr-btn", form: 'ineligibilite_form'
|
|
@ -1,4 +1,6 @@
|
|||
class Dossiers::EditFooterComponent < ApplicationComponent
|
||||
delegate :can_passer_en_construction?, to: :@dossier
|
||||
|
||||
def initialize(dossier:, annotation:)
|
||||
@dossier = dossier
|
||||
@annotation = annotation
|
||||
|
@ -14,20 +16,29 @@ class Dossiers::EditFooterComponent < ApplicationComponent
|
|||
@annotation.present?
|
||||
end
|
||||
|
||||
def disabled_submit_buttons_options
|
||||
{
|
||||
class: 'fr-text--sm fr-mb-0 fr-mr-2w',
|
||||
data: { 'fr-opened': "true" },
|
||||
aria: { controls: 'modal-eligibilite-rules-dialog' }
|
||||
}
|
||||
end
|
||||
|
||||
def submit_draft_button_options
|
||||
{
|
||||
class: 'fr-btn fr-btn--sm',
|
||||
disabled: !owner?,
|
||||
disabled: !owner? || !can_passer_en_construction?,
|
||||
method: :post,
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' }
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server }
|
||||
}
|
||||
end
|
||||
|
||||
def submit_en_construction_button_options
|
||||
{
|
||||
class: 'fr-btn fr-btn--sm',
|
||||
disabled: !can_passer_en_construction?,
|
||||
method: :post,
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' },
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server },
|
||||
form: { id: "form-submit-en-construction" }
|
||||
}
|
||||
end
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
en:
|
||||
submit: Submit the file
|
||||
submit_changes: Submit file changes
|
||||
submit_disabled: File submission disabled
|
||||
submitting: Submitting…
|
||||
invite_notice: You are invited to make amendments to this file but <strong>only the owner themselves can submit it</strong>.
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
fr:
|
||||
submit: Déposer le dossier
|
||||
submit_changes: Déposer les modifications
|
||||
submit_disabled: Pourquoi je ne peux pas déposer mon dossier ?
|
||||
submitting: Envoi en cours…
|
||||
invite_notice: En tant qu’invité, vous pouvez remplir ce formulaire – mais <strong>le titulaire du dossier doit le déposer lui-même</strong>.
|
||||
|
|
|
@ -3,8 +3,13 @@
|
|||
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
|
||||
|
||||
- if !annotation? && @dossier.can_transition_to_en_construction?
|
||||
- if !can_passer_en_construction?
|
||||
= link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
|
||||
= button_to t('.submit'), brouillon_dossier_url(@dossier), submit_draft_button_options
|
||||
- elsif @dossier.forked_with_changes?
|
||||
|
||||
- if @dossier.forked_with_changes?
|
||||
- if !can_passer_en_construction?
|
||||
= link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
|
||||
= button_to t('.submit_changes'), modifier_dossier_url(@dossier.editing_fork_origin), submit_en_construction_button_options
|
||||
|
||||
|
||||
|
|
|
@ -3,17 +3,15 @@
|
|||
class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
|
||||
ErrorDescriptor = Data.define(:anchor, :label, :error_message)
|
||||
|
||||
def initialize(dossier:, errors:)
|
||||
def initialize(dossier:)
|
||||
@dossier = dossier
|
||||
@errors = errors
|
||||
end
|
||||
|
||||
def dedup_and_partitioned_errors
|
||||
formated_errors = @errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
|
||||
@dossier.errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
|
||||
.to_a # but enum.to_a gives back an array
|
||||
.uniq { |error| [error.inner_error.base] } # dedup cumulated errors from dossier.champs, dossier.champs_public, dossier.champs_private which run the validator one time per association
|
||||
.map { |error| to_error_descriptor(error) }
|
||||
yield(Array(formated_errors[0..2]), Array(formated_errors[3..]))
|
||||
end
|
||||
|
||||
def to_error_descriptor(error)
|
||||
|
@ -27,6 +25,6 @@ class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
|
|||
end
|
||||
|
||||
def render?
|
||||
!@errors.empty?
|
||||
!@dossier.errors.empty?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,4 +5,3 @@ en:
|
|||
Your file has 1 error. <a href="%{url}">Fix-it</a> to continue :
|
||||
other: |
|
||||
Your file has %{count} errors. <a href="%{url}">Fix-them</a> to continue :
|
||||
see_more: Show all errors
|
||||
|
|
|
@ -5,4 +5,3 @@ fr:
|
|||
Votre dossier contient 1 champ en erreur. <a href="%{url}">Corrigez-la</a> pour poursuivre :
|
||||
other: |
|
||||
Votre dossier contient %{count} champs en erreurs. <a href="%{url}">Corrigez-les</a> pour poursuivre :
|
||||
see_more: Afficher toutes les erreurs
|
||||
|
|
|
@ -1,15 +1,4 @@
|
|||
.fr-alert.fr-alert--error.fr-mb-3w{ role: "alertdialog" }
|
||||
- dedup_and_partitioned_errors do |head, tail|
|
||||
%p#sumup-errors= t('.sumup_html', count: head.size + tail.size, url: head.first.anchor)
|
||||
%ul.fr-mb-0#head-errors
|
||||
- head.each do |error_descriptor|
|
||||
%li
|
||||
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
|
||||
= error_descriptor.error_message
|
||||
- if tail.size > 0
|
||||
%button{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline" }= t('.see_more')
|
||||
%ul#tail-errors.fr-collapse.fr-mt-0
|
||||
- tail.each do |error_descriptor|
|
||||
%li
|
||||
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
|
||||
= "(#{error_descriptor.error_message})"
|
||||
- if dedup_and_partitioned_errors.size > 0
|
||||
%p#sumup-errors= t('.sumup_html', count: dedup_and_partitioned_errors.size, url: dedup_and_partitioned_errors.first.anchor)
|
||||
= render ExpandableErrorList.new(errors: dedup_and_partitioned_errors)
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -0,0 +1,5 @@
|
|||
fr:
|
||||
modal:
|
||||
title: "Vous ne pouvez pas déposer votre dossier"
|
||||
close: "Fermer"
|
||||
close_alt: "Fermer la fenêtre modale"
|
|
@ -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
|
9
app/components/expandable_error_list.rb
Normal file
9
app/components/expandable_error_list.rb
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
en:
|
||||
see_more: Show all errors
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
fr:
|
||||
see_more: Afficher toutes les erreurs
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fr:
|
||||
title: Inéligibilité des dossiers
|
||||
state:
|
||||
pending: Désactivé
|
||||
ready: À configurer
|
||||
completed: Activé
|
||||
subtitle: Gérez vos conditions d’inéligibilité en fonction des champs du formulaire
|
|
@ -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')
|
|
@ -1,4 +1,6 @@
|
|||
class Procedure::ErrorsSummary < ApplicationComponent
|
||||
ErrorDescriptor = Data.define(:anchor, :label, :error_message)
|
||||
|
||||
def initialize(procedure:, validation_context:)
|
||||
@procedure = procedure
|
||||
@validation_context = validation_context
|
||||
|
@ -24,14 +26,14 @@ class Procedure::ErrorsSummary < ApplicationComponent
|
|||
@procedure.errors.present?
|
||||
end
|
||||
|
||||
def error_messages
|
||||
@procedure.errors.map do |error|
|
||||
[error, error_correction_page(error)]
|
||||
end
|
||||
def errors
|
||||
@procedure.errors.map { to_error_descriptor(_1) }
|
||||
end
|
||||
|
||||
def error_correction_page(error)
|
||||
case error.attribute
|
||||
when :ineligibilite_rules
|
||||
edit_admin_procedure_ineligibilite_rules_path(@procedure)
|
||||
when :draft_types_de_champ_public
|
||||
tdc = error.options[:type_de_champ]
|
||||
champs_admin_procedure_path(@procedure, anchor: dom_id(tdc.stable_self, :editor_error))
|
||||
|
@ -45,4 +47,14 @@ class Procedure::ErrorsSummary < ApplicationComponent
|
|||
edit_admin_procedure_mail_template_path(@procedure, klass.const_get(:SLUG))
|
||||
end
|
||||
end
|
||||
|
||||
def to_error_descriptor(error)
|
||||
libelle = case error.attribute
|
||||
when :draft_types_de_champ_public, :draft_types_de_champ_private
|
||||
error.options[:type_de_champ].libelle.truncate(200)
|
||||
else
|
||||
error.base.class.human_attribute_name(error.attribute)
|
||||
end
|
||||
ErrorDescriptor.new(error_correction_page(error), libelle, error.message)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,4 @@
|
|||
- if invalid?
|
||||
= render Dsfr::AlertComponent.new(state: :error, title: , extra_class_names: 'fr-mb-2w') do |c|
|
||||
- c.with_body do
|
||||
- error_messages.each do |(error, path)|
|
||||
%p.mt-2
|
||||
= error.full_message
|
||||
- if path.present?
|
||||
= "(#{link_to 'corriger', path, class: 'fr-link'})"
|
||||
= render ExpandableErrorList.new(errors:)
|
||||
|
|
10
app/components/procedure/pending_republish_component.rb
Normal file
10
app/components/procedure/pending_republish_component.rb
Normal 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
|
|
@ -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>
|
|
@ -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))
|
|
@ -1,9 +1,13 @@
|
|||
class Procedure::RevisionChangesComponent < ApplicationComponent
|
||||
def initialize(changes:, previous_revision:)
|
||||
@changes = changes
|
||||
def initialize(new_revision:, previous_revision:)
|
||||
@previous_revision = previous_revision
|
||||
@public_move_changes, @private_move_changes = changes.filter { _1.op == :move }.partition { !_1.private? }
|
||||
@delete_champ_warning = !total_dossiers.zero? && !@changes.all?(&:can_rebase?)
|
||||
@new_revision = new_revision
|
||||
|
||||
@tdc_changes = previous_revision.compare_types_de_champ(new_revision)
|
||||
@public_move_changes, @private_move_changes = @tdc_changes.filter { _1.op == :move }.partition { !_1.private? }
|
||||
@delete_champ_warning = !total_dossiers.zero? && !@tdc_changes.all?(&:can_rebase?)
|
||||
|
||||
@ineligibilite_rules_changes = previous_revision.compare_ineligibilite_rules(new_revision)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -80,3 +80,10 @@ fr:
|
|||
update_expression_reguliere_exemple_text: L’exemple d’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouvel exemple est « %{to} ».
|
||||
remove_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été supprimé.
|
||||
update_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouveau message est « %{to} ».
|
||||
ineligibilite_rules:
|
||||
add: La condition d’inéligibilité « %{new_condition} » a été ajoutée.
|
||||
remove: La condition d’inéligibilité « %{previous_condition} » a été supprimée
|
||||
update: La conditon d’inéligibilité « %{previous_condition} » a été changée pour « %{new_condition} »
|
||||
enabled: "L’inéligibilité des dossiers a été activée"
|
||||
disabled: "L’inéligibilité des dossiers a été désactivée"
|
||||
message_updated: "Le message d’inéligibilité a été changé pour « %{ineligibilite_message} »"
|
|
@ -2,7 +2,7 @@
|
|||
- list.with_empty do
|
||||
= t('.no_changes')
|
||||
|
||||
- @changes.each do |change|
|
||||
- @tdc_changes.each do |change|
|
||||
- prefix = change.private? ? 'private' : 'public'
|
||||
- case change.op
|
||||
- when :add
|
||||
|
@ -176,3 +176,7 @@
|
|||
- list.with_item do
|
||||
.fr-alert.fr-alert--warning.fr-mt-1v
|
||||
= t(".invalid_routing_rules_alert")
|
||||
|
||||
- @ineligibilite_rules_changes.each do |change|
|
||||
- list.with_item do
|
||||
= t(".ineligibilite_rules.#{change.op}", **change.i18n_params)
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.flex.justify-start.width-33
|
||||
.cell.flex.justify-start.column.flex-grow
|
||||
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ)
|
||||
= form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
|
||||
= form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules? || coordinate.used_by_ineligibilite_rules?
|
||||
|
||||
.flex.column.justify-start.flex-grow
|
||||
.cell
|
||||
|
@ -136,6 +136,10 @@
|
|||
%span
|
||||
utilisé pour
|
||||
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
|
||||
- elsif coordinate.used_by_ineligibilite_rules?
|
||||
%span
|
||||
utilisé pour
|
||||
= link_to('l’eligibilité des dossiers', edit_admin_procedure_ineligibilite_rules_path(revision.procedure_id))
|
||||
- else
|
||||
= button_to type_de_champ_path, class: 'fr-btn fr-btn--tertiary-no-outline fr-icon-delete-line', title: "Supprimer le champ", method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
|
||||
%span.sr-only Supprimer
|
||||
|
|
|
@ -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
|
|
@ -231,9 +231,9 @@ module Users
|
|||
|
||||
def submit_brouillon
|
||||
@dossier = dossier_with_champs(pj_template: false)
|
||||
@errors = submit_dossier_and_compute_errors
|
||||
submit_dossier_and_compute_errors
|
||||
|
||||
if @errors.blank?
|
||||
if @dossier.errors.blank? && @dossier.can_passer_en_construction?
|
||||
@dossier.passer_en_construction!
|
||||
@dossier.process_declarative!
|
||||
@dossier.process_sva_svr!
|
||||
|
@ -278,9 +278,9 @@ module Users
|
|||
editing_fork_origin.resolve_pending_correction
|
||||
end
|
||||
|
||||
@errors = submit_dossier_and_compute_errors
|
||||
submit_dossier_and_compute_errors
|
||||
|
||||
if @errors.blank?
|
||||
if @dossier.errors.blank? && @dossier.can_passer_en_construction?
|
||||
editing_fork_origin.merge_fork(@dossier)
|
||||
editing_fork_origin.submit_en_construction!
|
||||
|
||||
|
@ -288,7 +288,6 @@ module Users
|
|||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@dossier = editing_fork_origin
|
||||
render :modifier
|
||||
end
|
||||
|
||||
|
@ -303,10 +302,10 @@ module Users
|
|||
def update
|
||||
@dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier
|
||||
@dossier = dossier_with_champs(pj_template: false)
|
||||
@errors = update_dossier_and_compute_errors
|
||||
|
||||
@dossier.index_search_terms_later if @errors.empty?
|
||||
|
||||
@can_passer_en_construction_was = @dossier.can_passer_en_construction?
|
||||
update_dossier_and_compute_errors
|
||||
@dossier.index_search_terms_later if @dossier.errors.empty?
|
||||
@can_passer_en_construction_is = @dossier.can_passer_en_construction?
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?))
|
||||
|
@ -567,21 +566,14 @@ module Users
|
|||
|
||||
def submit_dossier_and_compute_errors
|
||||
@dossier.validate(:champs_public_value)
|
||||
|
||||
errors = @dossier.errors
|
||||
@dossier.check_mandatory_and_visible_champs.each do |error_on_champ|
|
||||
errors.import(error_on_champ)
|
||||
end
|
||||
@dossier.check_mandatory_and_visible_champs
|
||||
|
||||
if @dossier.editing_fork_origin&.pending_correction?
|
||||
@dossier.editing_fork_origin.validate(:champs_public_value)
|
||||
@dossier.editing_fork_origin.errors.where(:pending_correction).each do |error|
|
||||
errors.import(error)
|
||||
@dossier.errors.import(error)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
errors
|
||||
end
|
||||
|
||||
def ensure_ownership!
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,10 @@ module ChampConditionalConcern
|
|||
end
|
||||
end
|
||||
|
||||
def reset_visible # recompute after a dossier update
|
||||
remove_instance_variable :@visible if instance_variable_defined? :@visible
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def champs_for_condition
|
||||
|
|
|
@ -22,7 +22,7 @@ module DossierRebaseConcern
|
|||
end
|
||||
|
||||
def pending_changes
|
||||
procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
|
||||
procedure.published_revision.present? ? revision.compare_types_de_champ(procedure.published_revision) : []
|
||||
end
|
||||
|
||||
def can_rebase_mandatory_change?(stable_id)
|
||||
|
|
|
@ -156,7 +156,7 @@ class Dossier < ApplicationRecord
|
|||
state :sans_suite
|
||||
|
||||
event :passer_en_construction, after: :after_passer_en_construction, after_commit: :after_commit_passer_en_construction do
|
||||
transitions from: :brouillon, to: :en_construction
|
||||
transitions from: :brouillon, to: :en_construction, guard: :can_passer_en_construction?
|
||||
end
|
||||
|
||||
event :passer_en_instruction, after: :after_passer_en_instruction, after_commit: :after_commit_passer_en_instruction do
|
||||
|
@ -562,6 +562,12 @@ class Dossier < ApplicationRecord
|
|||
procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
|
||||
end
|
||||
|
||||
def can_passer_en_construction?
|
||||
return true if !revision.ineligibilite_enabled
|
||||
|
||||
!revision.ineligibilite_rules.compute(champs_for_revision(scope: :public))
|
||||
end
|
||||
|
||||
def can_passer_en_instruction?
|
||||
return false if blocked_with_pending_correction?
|
||||
|
||||
|
@ -936,6 +942,7 @@ class Dossier < ApplicationRecord
|
|||
.map do |champ|
|
||||
champ.errors.add(:value, :missing)
|
||||
end
|
||||
.each { errors.import(_1) }
|
||||
end
|
||||
|
||||
def demander_un_avis!(avis)
|
||||
|
|
|
@ -293,7 +293,7 @@ class Procedure < ApplicationRecord
|
|||
|
||||
validates_with MonAvisEmbedValidator
|
||||
|
||||
validates_associated :draft_revision, on: :publication
|
||||
validate :validates_associated_draft_revision_with_context
|
||||
validates_associated :initiated_mail, on: :publication
|
||||
validates_associated :received_mail, on: :publication
|
||||
validates_associated :closed_mail, on: :publication
|
||||
|
@ -431,11 +431,15 @@ class Procedure < ApplicationRecord
|
|||
|
||||
def draft_changed?
|
||||
preload_draft_and_published_revisions
|
||||
!brouillon? && published_revision.different_from?(draft_revision) && revision_changes.present?
|
||||
!brouillon? && (types_de_champ_revision_changes.present? || ineligibilite_rules_revision_changes.present?)
|
||||
end
|
||||
|
||||
def revision_changes
|
||||
published_revision.compare(draft_revision)
|
||||
def types_de_champ_revision_changes
|
||||
published_revision.compare_types_de_champ(draft_revision)
|
||||
end
|
||||
|
||||
def ineligibilite_rules_revision_changes
|
||||
published_revision.compare_ineligibilite_rules(draft_revision)
|
||||
end
|
||||
|
||||
def preload_draft_and_published_revisions
|
||||
|
@ -1017,6 +1021,13 @@ class Procedure < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def validates_associated_draft_revision_with_context
|
||||
return if draft_revision.blank?
|
||||
return if draft_revision.validate(validation_context)
|
||||
|
||||
draft_revision.errors.map { errors.import(_1) }
|
||||
end
|
||||
|
||||
def validate_auto_archive_on_in_the_future
|
||||
return if auto_archive_on.nil?
|
||||
return if auto_archive_on.future?
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class ProcedureRevision < ApplicationRecord
|
||||
include Logic
|
||||
self.implicit_order_column = :created_at
|
||||
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
|
||||
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
|
||||
|
@ -17,8 +18,19 @@ class ProcedureRevision < ApplicationRecord
|
|||
|
||||
scope :ordered, -> { order(:created_at) }
|
||||
|
||||
validates :ineligibilite_message, presence: true, if: -> { ineligibilite_enabled? }
|
||||
|
||||
delegate :path, to: :procedure, prefix: true
|
||||
|
||||
validate :ineligibilite_rules_are_valid?,
|
||||
on: [:ineligibilite_rules_editor, :publication]
|
||||
validates :ineligibilite_message,
|
||||
presence: true,
|
||||
if: -> { ineligibilite_enabled? },
|
||||
on: [:ineligibilite_rules_editor, :publication]
|
||||
|
||||
serialize :ineligibilite_rules, LogicSerializer
|
||||
|
||||
def build_champs_public
|
||||
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
|
||||
types_de_champ_public.reload.map(&:build_champ)
|
||||
|
@ -136,16 +148,18 @@ class ProcedureRevision < ApplicationRecord
|
|||
!draft?
|
||||
end
|
||||
|
||||
def different_from?(revision)
|
||||
revision_types_de_champ != revision.revision_types_de_champ
|
||||
end
|
||||
|
||||
def compare(revision)
|
||||
def compare_types_de_champ(revision)
|
||||
changes = []
|
||||
changes += compare_revision_types_de_champ(revision_types_de_champ, revision.revision_types_de_champ)
|
||||
changes
|
||||
end
|
||||
|
||||
def compare_ineligibilite_rules(revision)
|
||||
changes = []
|
||||
changes += compare_revision_ineligibilite_rules(revision)
|
||||
changes
|
||||
end
|
||||
|
||||
def dossier_for_preview(user)
|
||||
dossier = Dossier
|
||||
.create_with(autorisation_donnees: true)
|
||||
|
@ -251,6 +265,10 @@ class ProcedureRevision < ApplicationRecord
|
|||
types_de_champ_public.filter(&:routable?)
|
||||
end
|
||||
|
||||
def conditionable_types_de_champ
|
||||
types_de_champ_for(scope: :public).filter(&:conditionable?)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_estimated_fill_duration
|
||||
|
@ -318,6 +336,29 @@ class ProcedureRevision < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def compare_revision_ineligibilite_rules(new_revision)
|
||||
from_ineligibilite_rules = ineligibilite_rules
|
||||
to_ineligibilite_rules = new_revision.ineligibilite_rules
|
||||
changes = []
|
||||
|
||||
if from_ineligibilite_rules.present? && to_ineligibilite_rules.blank?
|
||||
changes << ProcedureRevisionChange::RemoveEligibiliteRuleChange
|
||||
end
|
||||
if from_ineligibilite_rules.blank? && to_ineligibilite_rules.present?
|
||||
changes << ProcedureRevisionChange::AddEligibiliteRuleChange
|
||||
end
|
||||
if from_ineligibilite_rules != to_ineligibilite_rules
|
||||
changes << ProcedureRevisionChange::UpdateEligibiliteRuleChange
|
||||
end
|
||||
if ineligibilite_message != new_revision.ineligibilite_message
|
||||
changes << ProcedureRevisionChange::UpdateEligibiliteMessageChange
|
||||
end
|
||||
if ineligibilite_enabled != new_revision.ineligibilite_enabled
|
||||
changes << (new_revision.ineligibilite_enabled ? ProcedureRevisionChange::EligibiliteEnabledChange : ProcedureRevisionChange::EligibiliteDisabledChange)
|
||||
end
|
||||
changes.map { _1.new(self, new_revision) }
|
||||
end
|
||||
|
||||
def compare_type_de_champ(from_type_de_champ, to_type_de_champ, from_coordinates, to_coordinates)
|
||||
changes = []
|
||||
if from_type_de_champ.type_champ != to_type_de_champ.type_champ
|
||||
|
@ -442,6 +483,13 @@ class ProcedureRevision < ApplicationRecord
|
|||
changes
|
||||
end
|
||||
|
||||
def ineligibilite_rules_are_valid?
|
||||
if ineligibilite_rules
|
||||
ineligibilite_rules.errors(types_de_champ_for(scope: :public).to_a)
|
||||
.each { errors.add(:ineligibilite_rules, :invalid) }
|
||||
end
|
||||
end
|
||||
|
||||
def replace_type_de_champ_by_clone(coordinate)
|
||||
cloned_type_de_champ = coordinate.type_de_champ.deep_clone do |original, kopy|
|
||||
ClonePiecesJustificativesService.clone_attachments(original, kopy)
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
class ProcedureRevisionChange
|
||||
attr_reader :type_de_champ
|
||||
def initialize(type_de_champ)
|
||||
@type_de_champ = type_de_champ
|
||||
class TypeDeChange
|
||||
attr_reader :type_de_champ
|
||||
def initialize(type_de_champ)
|
||||
@type_de_champ = type_de_champ
|
||||
end
|
||||
|
||||
def label = @type_de_champ.libelle
|
||||
def stable_id = @type_de_champ.stable_id
|
||||
def private? = @type_de_champ.private?
|
||||
def child? = @type_de_champ.child?
|
||||
|
||||
def to_h = { op:, stable_id:, label:, private: private? }
|
||||
end
|
||||
|
||||
def label = @type_de_champ.libelle
|
||||
def stable_id = @type_de_champ.stable_id
|
||||
def private? = @type_de_champ.private?
|
||||
def child? = @type_de_champ.child?
|
||||
|
||||
def to_h = { op:, stable_id:, label:, private: private? }
|
||||
|
||||
class AddChamp < ProcedureRevisionChange
|
||||
class AddChamp < TypeDeChange
|
||||
def initialize(type_de_champ)
|
||||
super(type_de_champ)
|
||||
end
|
||||
|
@ -23,7 +25,7 @@ class ProcedureRevisionChange
|
|||
def to_h = super.merge(mandatory: mandatory?)
|
||||
end
|
||||
|
||||
class RemoveChamp < ProcedureRevisionChange
|
||||
class RemoveChamp < TypeDeChange
|
||||
def initialize(type_de_champ)
|
||||
super(type_de_champ)
|
||||
end
|
||||
|
@ -32,7 +34,7 @@ class ProcedureRevisionChange
|
|||
def can_rebase?(dossier = nil) = true
|
||||
end
|
||||
|
||||
class MoveChamp < ProcedureRevisionChange
|
||||
class MoveChamp < TypeDeChange
|
||||
attr_reader :from, :to
|
||||
|
||||
def initialize(type_de_champ, from, to)
|
||||
|
@ -46,7 +48,7 @@ class ProcedureRevisionChange
|
|||
def to_h = super.merge(from:, to:)
|
||||
end
|
||||
|
||||
class UpdateChamp < ProcedureRevisionChange
|
||||
class UpdateChamp < TypeDeChange
|
||||
attr_reader :attribute, :from, :to
|
||||
|
||||
def initialize(type_de_champ, attribute, from, to)
|
||||
|
@ -75,4 +77,48 @@ class ProcedureRevisionChange
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class EligibiliteRulesChange
|
||||
attr_reader :previous_revision, :new_revision
|
||||
def initialize(previous_revision, new_revision)
|
||||
@previous_revision = previous_revision
|
||||
@new_revision = new_revision
|
||||
@previous_ineligibilite_rules = @previous_revision.ineligibilite_rules
|
||||
@new_ineligibilite_rules = @new_revision.ineligibilite_rules
|
||||
end
|
||||
|
||||
def i18n_params
|
||||
{
|
||||
previous_condition: @previous_ineligibilite_rules&.to_s(previous_revision.types_de_champ.filter { @previous_ineligibilite_rules.sources.include? _1.stable_id }),
|
||||
new_condition: @new_ineligibilite_rules&.to_s(new_revision.types_de_champ.filter { @new_ineligibilite_rules.sources.include? _1.stable_id })
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class AddEligibiliteRuleChange < EligibiliteRulesChange
|
||||
def op = :add
|
||||
end
|
||||
|
||||
class RemoveEligibiliteRuleChange < EligibiliteRulesChange
|
||||
def op = :remove
|
||||
end
|
||||
|
||||
class UpdateEligibiliteRuleChange < EligibiliteRulesChange
|
||||
def op = :update
|
||||
end
|
||||
|
||||
class EligibiliteEnabledChange < EligibiliteRulesChange
|
||||
def op = :enabled
|
||||
def i18n_params = {}
|
||||
end
|
||||
|
||||
class EligibiliteDisabledChange < EligibiliteRulesChange
|
||||
def op = :disabled
|
||||
def i18n_params = {}
|
||||
end
|
||||
|
||||
class UpdateEligibiliteMessageChange < EligibiliteRulesChange
|
||||
def op = :message_updated
|
||||
def i18n_params = { ineligibilite_message: @new_revision.ineligibilite_message }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -75,4 +75,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
|
|||
def used_by_routing_rules?
|
||||
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
|
||||
end
|
||||
|
||||
def used_by_ineligibilite_rules?
|
||||
revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -657,6 +657,10 @@ class TypeDeChamp < ApplicationRecord
|
|||
type_champ.in?(ROUTABLE_TYPES)
|
||||
end
|
||||
|
||||
def conditionable?
|
||||
Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(type_champ)
|
||||
end
|
||||
|
||||
def invalid_regexp?
|
||||
self.errors.delete(:expression_reguliere)
|
||||
self.errors.delete(:expression_reguliere_exemple_text)
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
28
app/views/administrateurs/ineligibilite_rules/edit.html.haml
Normal file
28
app/views/administrateurs/ineligibilite_rules/edit.html.haml
Normal 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 d’inéligibilité ne pourront pas être déposés. Plus d’informations sur l’inéligibilité des dossiers dans la
|
||||
= link_to('doc', ELIGIBILITE_URL, title: "Document sur l’inéligibilité des dossiers", **external_link_attributes)
|
||||
|
||||
- if !@procedure.draft_revision.conditionable_types_de_champ.present?
|
||||
%p.fr-mt-2w.fr-mb-2w
|
||||
Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire :
|
||||
%ul
|
||||
- Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.each do
|
||||
%li= "« #{t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »"
|
||||
%p.fr-mt-2w
|
||||
= link_to 'Ajouter un champ supportant les conditions d’inéligibilité', champs_admin_procedure_path(@procedure), class: 'fr-link fr-icon-arrow-right-line fr-link--icon-right'
|
||||
= render Procedure::FixedFooterComponent.new(procedure: @procedure)
|
||||
- else
|
||||
= render Conditions::IneligibiliteRulesComponent.new(draft_revision: @procedure.draft_revision)
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -8,7 +8,7 @@
|
|||
%p.mb-2= t('.draft_changed_procedure_alert')
|
||||
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
|
||||
- c.with_body do
|
||||
= render Procedure::RevisionChangesComponent.new changes: procedure.revision_changes, previous_revision: procedure.published_revision
|
||||
= render Procedure::RevisionChangesComponent.new new_revision: procedure.draft_revision, previous_revision: procedure.published_revision
|
||||
- if procedure.close?
|
||||
= render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f }
|
||||
- elsif @procedure.brouillon? && @procedure.missing_steps.empty?
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
- previous_revision = nil
|
||||
- @procedure.revisions.each do |revision|
|
||||
- if previous_revision.present? && !revision.draft?
|
||||
- changes = previous_revision.compare(revision)
|
||||
- dossiers = revision.dossiers.visible_by_administration
|
||||
- dossiers_en_construction_count = dossiers.state_en_construction.count
|
||||
- dossiers_en_instruction_count = dossiers.state_en_instruction.count
|
||||
|
@ -31,7 +30,7 @@
|
|||
%p= t('.dossiers_en_construction', count: dossiers_en_construction_count)
|
||||
- elsif !dossiers_en_instruction_count.zero?
|
||||
%p= t('.dossiers_en_instruction', count: dossiers_en_instruction_count)
|
||||
= render Procedure::RevisionChangesComponent.new changes:, previous_revision:
|
||||
= render Procedure::RevisionChangesComponent.new new_revision: revision, previous_revision:
|
||||
- previous_revision = revision
|
||||
|
||||
= render Procedure::FixedFooterComponent.new(procedure: @procedure)
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
- if @procedure.draft_changed?
|
||||
= render Dsfr::CalloutComponent.new(title: t(:has_changes, scope: [:administrateurs, :revision_changes]), icon: "fr-fi-information-line") do |c|
|
||||
- c.with_body do
|
||||
= render Procedure::RevisionChangesComponent.new changes: @procedure.revision_changes, previous_revision: @procedure.published_revision
|
||||
= render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
|
||||
= render Procedure::RevisionChangesComponent.new new_revision: @procedure.draft_revision, previous_revision: @procedure.published_revision
|
||||
|
||||
- c.with_bottom do
|
||||
%ul.fr-mt-2w.fr-btns-group.fr-btns-group--inline
|
||||
|
@ -71,6 +71,7 @@
|
|||
= render Procedure::Card::PresentationComponent.new(procedure: @procedure)
|
||||
= render Procedure::Card::ZonesComponent.new(procedure: @procedure) if Rails.application.config.ds_zonage_enabled
|
||||
= render Procedure::Card::ChampsComponent.new(procedure: @procedure)
|
||||
= render Procedure::Card::IneligibiliteDossierComponent.new(procedure: @procedure)
|
||||
= render Procedure::Card::ServiceComponent.new(procedure: @procedure, administrateur: current_administrateur)
|
||||
= render Procedure::Card::AdministrateursComponent.new(procedure: @procedure)
|
||||
= render Procedure::Card::InstructeursComponent.new(procedure: @procedure)
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
= render NestedForms::FormOwnerComponent.new
|
||||
= form_for dossier_for_editing, url: brouillon_dossier_url(dossier), method: :patch, html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } do |f|
|
||||
|
||||
= render Dossiers::ErrorsFullMessagesComponent.new(dossier: @dossier, errors: @errors || [])
|
||||
= render Dossiers::ErrorsFullMessagesComponent.new(dossier: dossier)
|
||||
%header.mb-6
|
||||
.fr-highlight
|
||||
%p.fr-text--sm
|
||||
|
@ -25,4 +25,6 @@
|
|||
|
||||
= render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier)
|
||||
|
||||
= render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier)
|
||||
|
||||
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
= render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier }
|
||||
|
||||
- if !params.key?(:validate)
|
||||
- if @can_passer_en_construction_was && !@can_passer_en_construction_is
|
||||
= turbo_stream.append('contenu', render(Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: @dossier)))
|
||||
- else @ineligibilite_rules_is_computable
|
||||
= turbo_stream.remove(dom_id(@dossier, :ineligibilite_rules_broken))
|
||||
|
|
|
@ -61,6 +61,9 @@ DS_ENV="staging"
|
|||
# Instance customization: URL of the Routage documentation
|
||||
# ROUTAGE_URL=""
|
||||
#
|
||||
# Instance customization: URL of the EligibiliteDossier documentation
|
||||
# ELIGIBILITE_URL=""
|
||||
#
|
||||
# Instance customization: URL of the accessibility statement
|
||||
# ACCESSIBILITE_URL=""
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ CGU_URL = ENV.fetch("CGU_URL", [DOC_URL, "cgu"].join("/"))
|
|||
MENTIONS_LEGALES_URL = ENV.fetch("MENTIONS_LEGALES_URL", "/mentions-legales")
|
||||
ACCESSIBILITE_URL = ENV.fetch("ACCESSIBILITE_URL", "/declaration-accessibilite")
|
||||
ROUTAGE_URL = ENV.fetch("ROUTAGE_URL", [DOC_URL, "/pour-aller-plus-loin/routage"].join("/"))
|
||||
ELIGIBILITE_URL = ENV.fetch("ELIGIBILITE_URL", [DOC_URL, "/pour-aller-plus-loin/eligibilite-des-dossiers"].join("/"))
|
||||
API_DOC_URL = [DOC_URL, "api-graphql"].join("/")
|
||||
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
|
||||
WEBHOOK_ALTERNATIVE_DOC_URL = [DOC_URL, "api-graphql", "cas-dusages-exemple-dimplementation", "synchroniser-les-dossiers-modifies-sur-ma-demarche"].join("/")
|
||||
|
|
|
@ -606,6 +606,7 @@ en:
|
|||
otp_attempt: 'OTP code (only if you have already activated 2FA)'
|
||||
procedure:
|
||||
zone: This procedure is run by
|
||||
ineligibilite_rules: "Eligibility rules"
|
||||
champs:
|
||||
value: Value
|
||||
default_mail_attributes: &default_mail_attributes
|
||||
|
@ -667,6 +668,10 @@ en:
|
|||
path:
|
||||
taken: is already used for procedure. You cannot use it because it belongs to another administrator.
|
||||
invalid: is not valid. It must countain between 3 and 200 characters among a-z, 0-9, '_' and '-'.
|
||||
procedure_revision:
|
||||
attributes:
|
||||
ineligibilite_rules:
|
||||
invalid: are invalid
|
||||
"dossier/champs":
|
||||
format: "%{message}"
|
||||
attributes:
|
||||
|
|
|
@ -610,6 +610,7 @@ fr:
|
|||
otp_attempt: 'Code OTP (uniquement si vous avez déjà activé 2FA)'
|
||||
procedure:
|
||||
zone: La démarche est mise en œuvre par
|
||||
ineligibilite_rules: "Les règles d’inéligibilité"
|
||||
champs:
|
||||
value: Valeur du champ
|
||||
default_mail_attributes: &default_mail_attributes
|
||||
|
@ -669,6 +670,10 @@ fr:
|
|||
path:
|
||||
taken: est déjà utilisé par une démarche. Vous ne pouvez pas l’utiliser car il appartient à un autre administrateur.
|
||||
invalid: n’est pas valide. Il doit comporter au moins 3 caractères, au plus 200 caractères et seuls les caractères a-z, 0-9, '_' et '-' sont autorisés.
|
||||
procedure_revision:
|
||||
attributes:
|
||||
ineligibilite_rules:
|
||||
invalid: ne sont pas valides
|
||||
"dossier/champs":
|
||||
format: "%{message}"
|
||||
attributes:
|
||||
|
|
|
@ -72,16 +72,16 @@ en:
|
|||
invalid: 'invalid format'
|
||||
draft_types_de_champ_public:
|
||||
format: 'Public field %{message}'
|
||||
invalid_condition: "« %{value} » have an invalid logic"
|
||||
empty_repetition: '« %{value} » requires at least one field'
|
||||
empty_drop_down: '« %{value} » requires at least one option'
|
||||
inconsistent_header_section: "« %{value} » %{custom_message}"
|
||||
invalid_condition: "have an invalid logic"
|
||||
empty_repetition: 'requires at least one field'
|
||||
empty_drop_down: 'requires at least one option'
|
||||
inconsistent_header_section: "%{custom_message}"
|
||||
draft_types_de_champ_private:
|
||||
format: 'Private field %{message}'
|
||||
invalid_condition: "« %{value} » have an invalid logic"
|
||||
empty_repetition: '« %{value} » requires at least one field'
|
||||
empty_drop_down: '« %{value} » requires at least one option'
|
||||
inconsistent_header_section: "« %{value} » %{custom_message}"
|
||||
invalid_condition: "have an invalid logic"
|
||||
empty_repetition: 'requires at least one field'
|
||||
empty_drop_down: 'requires at least one option'
|
||||
inconsistent_header_section: "%{custom_message}"
|
||||
attestation_template:
|
||||
format: "%{attribute} %{message}"
|
||||
initiated_mail:
|
||||
|
|
|
@ -8,7 +8,7 @@ fr:
|
|||
procedure:
|
||||
hints:
|
||||
description: Décrivez en quelques lignes le contexte, la finalité, etc.
|
||||
description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les critères d’éligibilité s’il y en a, les pré-requis, etc.
|
||||
description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les conditions d’éligibilité s’il y en a, les pré-requis, etc.
|
||||
description_pj: Décrivez la liste des pièces jointes à fournir s’il y en a
|
||||
lien_site_web: "Il s'agit de la page de votre site web où le lien sera diffusé. Ex: https://exemple.gouv.fr/page_informant_sur_ma_demarche"
|
||||
cadre_juridique: "Exemple: 'https://www.legifrance.gouv.fr/'"
|
||||
|
@ -78,16 +78,16 @@ fr:
|
|||
invalid: 'n’a pas le bon format'
|
||||
draft_types_de_champ_public:
|
||||
format: 'Le champ %{message}'
|
||||
invalid_condition: "« %{value} » a une logique conditionnelle invalide"
|
||||
empty_repetition: '« %{value} » doit comporter au moins un champ répétable'
|
||||
empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable'
|
||||
inconsistent_header_section: "« %{value} » %{custom_message}"
|
||||
invalid_condition: "a une logique conditionnelle invalide"
|
||||
empty_repetition: 'doit comporter au moins un champ répétable'
|
||||
empty_drop_down: 'doit comporter au moins un choix sélectionnable'
|
||||
inconsistent_header_section: "%{custom_message}"
|
||||
draft_types_de_champ_private:
|
||||
format: 'L’annotation privée %{message}'
|
||||
invalid_condition: "« %{value} » a une logique conditionnelle invalide"
|
||||
empty_repetition: '« %{value} » doit comporter au moins un champ répétable'
|
||||
empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable'
|
||||
inconsistent_header_section: "« %{value} » %{custom_message}"
|
||||
invalid_condition: "a une logique conditionnelle invalide"
|
||||
empty_repetition: 'doit comporter au moins un champ répétable'
|
||||
empty_drop_down: 'doit comporter au moins un choix sélectionnable'
|
||||
inconsistent_header_section: "%{custom_message}"
|
||||
attestation_template:
|
||||
format: "%{attribute} %{message}"
|
||||
initiated_mail:
|
||||
|
|
7
config/locales/models/procedure_revision/fr.yml
Normal file
7
config/locales/models/procedure_revision/fr.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
procedure_revision:
|
||||
ineligibilite_message: Message d’inéligibilité
|
||||
hints:
|
||||
ineligibilite_message: "Ce message sera affiché à l’usager si son dossier est bloqué et lui expliquera la raison de son inéligibilité."
|
|
@ -608,6 +608,14 @@ Rails.application.routes.draw do
|
|||
delete :delete_row, on: :member
|
||||
end
|
||||
|
||||
resource :ineligibilite_rules, only: [:edit, :update, :destroy], param: :revision_id do
|
||||
patch :change_targeted_champ, on: :member
|
||||
patch :update_all_rows, on: :member
|
||||
patch :add_row, on: :member
|
||||
delete :delete_row, on: :member
|
||||
patch :change
|
||||
end
|
||||
|
||||
patch :update_defaut_groupe_instructeur, controller: 'routing_rules', as: :update_defaut_groupe_instructeur
|
||||
|
||||
put 'clone'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddTransitionsRulesToProcedureRevisions < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :procedure_revisions, :ineligibilite_rules, :jsonb
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddDossierIneligbleMessageToProcedureRevisions < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :procedure_revisions, :ineligibilite_message, :text
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -863,6 +863,9 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_27_090508) do
|
|||
create_table "procedure_revisions", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.bigint "dossier_submitted_message_id"
|
||||
t.boolean "ineligibilite_enabled", default: false, null: false
|
||||
t.text "ineligibilite_message"
|
||||
t.jsonb "ineligibilite_rules"
|
||||
t.bigint "procedure_id", null: false
|
||||
t.datetime "published_at", precision: nil
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
|
|
|
@ -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
|
50
spec/components/dossiers/edit_footer_component_spec.rb
Normal file
50
spec/components/dossiers/edit_footer_component_spec.rb
Normal 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
|
|
@ -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
|
|
@ -11,27 +11,33 @@ describe Procedure::ErrorsSummary, type: :component do
|
|||
context 'when :publication' do
|
||||
let(:validation_context) { :publication }
|
||||
|
||||
it 'shows errors for public and private tdc' do
|
||||
expect(page).to have_text("Le champ « public » doit comporter au moins un choix sélectionnable")
|
||||
expect(page).to have_text("L’annotation privée « private » doit comporter au moins un choix sélectionnable")
|
||||
it 'shows errors and links for public and private tdc' do
|
||||
expect(page).to have_content("Erreur : Des problèmes empêchent la publication de la démarche")
|
||||
expect(page).to have_selector("a", text: "public")
|
||||
expect(page).to have_selector("a", text: "private")
|
||||
expect(page).to have_text("doit comporter au moins un choix sélectionnable", count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :types_de_champ_public_editor' do
|
||||
let(:validation_context) { :types_de_champ_public_editor }
|
||||
|
||||
it 'shows errors for public only tdc' do
|
||||
expect(page).to have_text("Le champ « public » doit comporter au moins un choix sélectionnable")
|
||||
expect(page).not_to have_text("L’annotation privée « private » doit comporter au moins un choix sélectionnable")
|
||||
it 'shows errors and links for public only tdc' do
|
||||
expect(page).to have_text("Erreur : Les champs formulaire contiennent des erreurs")
|
||||
expect(page).to have_selector("a", text: "public")
|
||||
expect(page).to have_text("doit comporter au moins un choix sélectionnable", count: 1)
|
||||
expect(page).not_to have_selector("a", text: "private")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :types_de_champ_private_editor' do
|
||||
let(:validation_context) { :types_de_champ_private_editor }
|
||||
|
||||
it 'shows errors for private only tdc' do
|
||||
expect(page).not_to have_text("Le champ « public » doit comporter au moins un choix sélectionnable")
|
||||
expect(page).to have_text("L’annotation privée « private » doit comporter au moins un choix sélectionnable")
|
||||
it 'shows errors and links for private only tdc' do
|
||||
expect(page).to have_text("Erreur : Les annotations privées contiennent des erreurs")
|
||||
expect(page).to have_selector("a", text: "private")
|
||||
expect(page).to have_text("doit comporter au moins un choix sélectionnable")
|
||||
expect(page).not_to have_selector("a", text: "public")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -52,16 +58,24 @@ describe Procedure::ErrorsSummary, type: :component do
|
|||
|
||||
before { subject }
|
||||
|
||||
it 'renders all errors on champ' do
|
||||
expect(page).to have_text("Le champ « drop down list requires options » doit comporter au moins un choix sélectionnable")
|
||||
expect(page).to have_text("Le champ « repetition requires children » doit comporter au moins un champ répétable")
|
||||
expect(page).to have_text("Le champ « invalid condition » a une logique conditionnelle invalide")
|
||||
expect(page).to have_text("Le champ « header sections must have consistent order » devrait être précédé d'un titre de niveau 1")
|
||||
# TODO, test attestation_template, initiated_mail, :received_mail, :closed_mail, :refused_mail, :without_continuation_mail, :re_instructed_mail
|
||||
it 'renders all errors and links on champ' do
|
||||
expect(page).to have_selector("a", text: "drop down list requires options")
|
||||
expect(page).to have_content("doit comporter au moins un choix sélectionnable")
|
||||
|
||||
expect(page).to have_selector("a", text: "repetition requires children")
|
||||
expect(page).to have_content("doit comporter au moins un champ répétable")
|
||||
|
||||
expect(page).to have_selector("a", text: "invalid condition")
|
||||
expect(page).to have_content("a une logique conditionnelle invalide")
|
||||
|
||||
expect(page).to have_selector("a", text: "header sections must have consistent order")
|
||||
expect(page).to have_content("devrait être précédé d'un titre de niveau 1")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'render error for other kind of associated objects' do
|
||||
include Logic
|
||||
|
||||
let(:validation_context) { :publication }
|
||||
let(:procedure) { create(:procedure, attestation_template:, initiated_mail:) }
|
||||
let(:attestation_template) { build(:attestation_template) }
|
||||
|
@ -69,12 +83,15 @@ describe Procedure::ErrorsSummary, type: :component do
|
|||
|
||||
before do
|
||||
[:attestation_template, :initiated_mail].map { procedure.send(_1).update_column(:body, '--invalidtag--') }
|
||||
procedure.draft_revision.update(ineligibilite_enabled: true, ineligibilite_rules: ds_eq(constant(true), constant(1)), ineligibilite_message: 'ko')
|
||||
subject
|
||||
end
|
||||
|
||||
it 'render error nicely' do
|
||||
expect(page).to have_text("Le modèle d’attestation n'est pas valide")
|
||||
expect(page).to have_text("L’email de notification de passage de dossier en instruction n'est pas valide")
|
||||
expect(page).to have_selector("a", text: "Les règles d’inéligibilité")
|
||||
expect(page).to have_selector("a", text: "Le modèle d’attestation")
|
||||
expect(page).to have_selector("a", text: "L’email de notification de passage de dossier en instruction")
|
||||
expect(page).to have_text("n'est pas valide", count: 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -2,10 +2,12 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
|
|||
describe 'render' do
|
||||
let(:component) { described_class.new(coordinate:, upper_coordinates: []) }
|
||||
let(:routing_rules_stable_ids) { [] }
|
||||
let(:ineligibilite_rules_used?) { false }
|
||||
|
||||
before do
|
||||
Flipper.enable_actor(:engagement_juridique_type_de_champ, procedure)
|
||||
allow_any_instance_of(Procedure).to receive(:stable_ids_used_by_routing_rules).and_return(routing_rules_stable_ids)
|
||||
allow_any_instance_of(ProcedureRevisionTypeDeChamp).to receive(:used_by_ineligibilite_rules?).and_return(ineligibilite_rules_used?)
|
||||
render_inline(component)
|
||||
end
|
||||
|
||||
|
@ -29,6 +31,15 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
|
|||
expect(page).to have_text(/utilisé pour\nle routage/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'drop down tdc used for ineligibilite_rules' do
|
||||
let(:ineligibilite_rules_used?) { true }
|
||||
|
||||
it do
|
||||
expect(page).to have_css("select[disabled=\"disabled\"]")
|
||||
expect(page).to have_text(/l’eligibilité des dossiers/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tdc ej' do
|
||||
|
|
|
@ -10,16 +10,18 @@ describe TypesDeChampEditor::EditorComponent, type: :component do
|
|||
context 'types_de_champ_public' do
|
||||
let(:is_annotation) { false }
|
||||
it 'does not render private champs errors' do
|
||||
expect(subject).not_to have_text("« private » doit comporter au moins un choix sélectionnable")
|
||||
expect(subject).to have_text("« public » doit comporter au moins un choix sélectionnable")
|
||||
expect(subject).not_to have_text("private")
|
||||
expect(subject).to have_selector("a", text: "public")
|
||||
expect(subject).to have_text("doit comporter au moins un choix sélectionnable")
|
||||
end
|
||||
end
|
||||
|
||||
context 'types_de_champ_private' do
|
||||
let(:is_annotation) { true }
|
||||
it 'does not render public champs errors' do
|
||||
expect(subject).to have_text("« private » doit comporter au moins un choix sélectionnable")
|
||||
expect(subject).not_to have_text("« public » doit comporter au moins un choix sélectionnable")
|
||||
expect(subject).to have_selector("a", text: "private")
|
||||
expect(subject).to have_text("doit comporter au moins un choix sélectionnable")
|
||||
expect(subject).not_to have_text("public")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
describe Administrateurs::IneligibiliteRulesController, type: :controller do
|
||||
include Logic
|
||||
let(:user) { create(:user) }
|
||||
let(:admin) { create(:administrateur, user: create(:user)) }
|
||||
let(:procedure) { create(:procedure, administrateurs: [admin], types_de_champ_public:) }
|
||||
let(:types_de_champ_public) { [] }
|
||||
|
||||
describe 'condition management' do
|
||||
before { sign_in(admin.user) }
|
||||
|
||||
let(:default_params) do
|
||||
{
|
||||
procedure_id: procedure.id,
|
||||
revision_id: procedure.draft_revision.id
|
||||
}
|
||||
end
|
||||
|
||||
describe '#add_row' do
|
||||
subject { post :add_row, params: default_params, format: :turbo_stream }
|
||||
|
||||
context 'without any row' do
|
||||
it 'creates an empty condition' do
|
||||
expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(nil)
|
||||
.to(empty_operator(empty, empty))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with row' do
|
||||
before do
|
||||
procedure.draft_revision.ineligibilite_rules = empty_operator(empty, empty)
|
||||
procedure.draft_revision.save!
|
||||
end
|
||||
|
||||
it 'add one more creates an empty condition' do
|
||||
expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(empty_operator(empty, empty))
|
||||
.to(ds_and([
|
||||
empty_operator(empty, empty),
|
||||
empty_operator(empty, empty)
|
||||
]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'delete_row' do
|
||||
let(:condition_form) do
|
||||
{
|
||||
top_operator_name: Logic::And.name,
|
||||
rows: [
|
||||
{
|
||||
targeted_champ: empty.to_json,
|
||||
operator_name: Logic::EmptyOperator,
|
||||
value: empty.to_json
|
||||
},
|
||||
{
|
||||
targeted_champ: empty.to_json,
|
||||
operator_name: Logic::EmptyOperator,
|
||||
value: empty.to_json
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
let(:initial_condition) do
|
||||
ds_and([
|
||||
empty_operator(empty, empty),
|
||||
empty_operator(empty, empty)
|
||||
])
|
||||
end
|
||||
|
||||
subject { delete :delete_row, params: default_params.merge(row_index: 0, procedure_revision: { condition_form: }), format: :turbo_stream }
|
||||
it 'remove condition' do
|
||||
procedure.draft_revision.update(ineligibilite_rules: initial_condition)
|
||||
|
||||
expect { subject }
|
||||
.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(initial_condition)
|
||||
.to(empty_operator(empty, empty))
|
||||
end
|
||||
end
|
||||
|
||||
context 'simple tdc' do
|
||||
let(:types_de_champ_public) { [{ type: :yes_no }] }
|
||||
let(:yes_no_tdc) { procedure.draft_revision.types_de_champ_for(scope: :public).first }
|
||||
let(:targeted_champ) { champ_value(yes_no_tdc.stable_id).to_json }
|
||||
|
||||
describe '#change_targeted_champ' do
|
||||
let(:condition_form) do
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
targeted_champ: targeted_champ,
|
||||
operator_name: Logic::Eq.name,
|
||||
value: constant(true).to_json
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
|
||||
it 'update condition' do
|
||||
expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(nil)
|
||||
.to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update' do
|
||||
let(:value) { constant(true).to_json }
|
||||
let(:operator_name) { Logic::Eq.name }
|
||||
let(:condition_form) do
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
targeted_champ: targeted_champ,
|
||||
operator_name: operator_name,
|
||||
value: value
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
subject { patch :update, params: default_params.merge(procedure_revision: { condition_form: condition_form }), format: :turbo_stream }
|
||||
it 'updates condition' do
|
||||
expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(nil)
|
||||
.to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'repetition tdc' do
|
||||
let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :yes_no }] }] }
|
||||
let(:yes_no_tdc) { procedure.draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'yes_no' } }
|
||||
let(:targeted_champ) { champ_value(yes_no_tdc.stable_id).to_json }
|
||||
let(:condition_form) do
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
targeted_champ: targeted_champ,
|
||||
operator_name: Logic::Eq.name,
|
||||
value: constant(true).to_json
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
|
||||
describe "#update" do
|
||||
it 'update condition' do
|
||||
expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(nil)
|
||||
.to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#change_targeted_champ' do
|
||||
let(:condition_form) do
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
targeted_champ: targeted_champ,
|
||||
operator_name: Logic::Eq.name,
|
||||
value: constant(true).to_json
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
|
||||
it 'update condition' do
|
||||
expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
|
||||
.from(nil)
|
||||
.to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#edit' do
|
||||
subject { get :edit, params: { procedure_id: procedure.id } }
|
||||
|
||||
context 'when user is not signed in' do
|
||||
it { is_expected.to redirect_to(new_user_session_path) }
|
||||
end
|
||||
|
||||
context 'when user is signed in but not admin of procedure' do
|
||||
before { sign_in(user) }
|
||||
it { is_expected.to redirect_to(new_user_session_path) }
|
||||
end
|
||||
|
||||
context 'when user is signed as admin' do
|
||||
before do
|
||||
sign_in(admin.user)
|
||||
subject
|
||||
end
|
||||
|
||||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
context 'rendered without tdc' do
|
||||
let(:types_de_champ_public) { [] }
|
||||
render_views
|
||||
|
||||
it { expect(response.body).to have_link("Ajouter un champ supportant les conditions d’inéligibilité") }
|
||||
end
|
||||
|
||||
context 'rendered with tdc' do
|
||||
let(:types_de_champ_public) { [{ type: :yes_no }] }
|
||||
render_views
|
||||
|
||||
it { expect(response.body).not_to have_link("Ajouter un champ supportant les conditions d’inéligibilité") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'change' do
|
||||
let(:params) do
|
||||
{
|
||||
procedure_id: procedure.id,
|
||||
procedure_revision: {
|
||||
ineligibilite_message: 'panpan',
|
||||
ineligibilite_enabled: '1'
|
||||
}
|
||||
}
|
||||
end
|
||||
before { sign_in(admin.user) }
|
||||
it 'works' do
|
||||
patch :change, params: params
|
||||
draft_revision = procedure.reload.draft_revision
|
||||
expect(draft_revision.ineligibilite_message).to eq('panpan')
|
||||
expect(draft_revision.ineligibilite_enabled).to eq(true)
|
||||
expect(response).to redirect_to(edit_admin_procedure_ineligibilite_rules_path(procedure))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -398,7 +398,9 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
describe '#submit_brouillon' do
|
||||
before { sign_in(user) }
|
||||
let!(:dossier) { create(:dossier, user: user) }
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
|
||||
let(:types_de_champ_public) { [{ type: :text }] }
|
||||
let!(:dossier) { create(:dossier, user:, procedure:) }
|
||||
let(:first_champ) { dossier.champs_public.first }
|
||||
let(:anchor_to_first_champ) { controller.helpers.link_to first_champ.libelle, brouillon_dossier_path(anchor: first_champ.labelledby_id), class: 'error-anchor' }
|
||||
let(:value) { 'beautiful value' }
|
||||
|
@ -439,9 +441,9 @@ describe Users::DossiersController, type: :controller do
|
|||
render_views
|
||||
let(:error_message) { 'nop' }
|
||||
before do
|
||||
expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
|
||||
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||
[double(inner_error: double(base: first_champ), message: 'nop')]
|
||||
allow_any_instance_of(Dossier).to receive(:validate).and_return(false)
|
||||
allow_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||
[instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')]
|
||||
)
|
||||
subject
|
||||
end
|
||||
|
@ -461,11 +463,8 @@ describe Users::DossiersController, type: :controller do
|
|||
render_views
|
||||
|
||||
let(:value) { nil }
|
||||
|
||||
before do
|
||||
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
|
||||
subject
|
||||
end
|
||||
let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] }
|
||||
before { subject }
|
||||
|
||||
it { expect(response).to render_template(:brouillon) }
|
||||
it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") }
|
||||
|
@ -548,8 +547,8 @@ describe Users::DossiersController, type: :controller do
|
|||
render_views
|
||||
|
||||
before do
|
||||
expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
|
||||
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||
allow_any_instance_of(Dossier).to receive(:validate).and_return(false)
|
||||
allow_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||
[double(inner_error: double(base: first_champ), message: 'nop')]
|
||||
)
|
||||
|
||||
|
@ -661,7 +660,8 @@ describe Users::DossiersController, type: :controller do
|
|||
describe '#update brouillon' do
|
||||
before { sign_in(user) }
|
||||
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}, { type: :piece_justificative }]) }
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
|
||||
let(:types_de_champ_public) { [{}, { type: :piece_justificative }] }
|
||||
let(:dossier) { create(:dossier, user:, procedure:) }
|
||||
let(:first_champ) { dossier.champs_public.first }
|
||||
let(:piece_justificative_champ) { dossier.champs_public.last }
|
||||
|
@ -754,13 +754,66 @@ describe Users::DossiersController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
it "debounce search terms indexation" do
|
||||
# dossier creation trigger a first indexation and flag,
|
||||
# so we we have to remove this flag
|
||||
dossier.debounce_index_search_terms_flag.remove
|
||||
context 'having ineligibilite_rules setup' do
|
||||
include Logic
|
||||
render_views
|
||||
|
||||
assert_enqueued_jobs(1, only: DossierIndexSearchTermsJob) do
|
||||
3.times { patch :update, params: payload, format: :turbo_stream }
|
||||
let(:types_de_champ_public) { [{ type: :text }, { type: :integer_number }] }
|
||||
let(:text_champ) { dossier.champs_public.first }
|
||||
let(:number_champ) { dossier.champs_public.last }
|
||||
let(:submit_payload) do
|
||||
{
|
||||
id: dossier.id,
|
||||
dossier: {
|
||||
groupe_instructeur_id: dossier.groupe_instructeur_id,
|
||||
champs_public_attributes: {
|
||||
text_champ.public_id => {
|
||||
with_public_id: true,
|
||||
value: "hello world"
|
||||
},
|
||||
number_champ.public_id => {
|
||||
with_public_id: true,
|
||||
value:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:must_be_greater_than) { 10 }
|
||||
|
||||
before do
|
||||
procedure.published_revision.update(
|
||||
ineligibilite_enabled: true,
|
||||
ineligibilite_message: 'lol',
|
||||
ineligibilite_rules: greater_than(champ_value(number_champ.stable_id), constant(must_be_greater_than))
|
||||
)
|
||||
procedure.published_revision.save!
|
||||
end
|
||||
render_views
|
||||
|
||||
context 'when it switches from true to false' do
|
||||
let(:value) { must_be_greater_than + 1 }
|
||||
|
||||
it 'raises popup' do
|
||||
subject
|
||||
dossier.reload
|
||||
expect(dossier.can_passer_en_construction?).to be_falsey
|
||||
expect(assigns(:can_passer_en_construction_was)).to eq(true)
|
||||
expect(assigns(:can_passer_en_construction_is)).to eq(false)
|
||||
expect(response.body).to match(ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it stays true' do
|
||||
let(:value) { must_be_greater_than - 1 }
|
||||
it 'does nothing' do
|
||||
subject
|
||||
dossier.reload
|
||||
expect(dossier.can_passer_en_construction?).to be_truthy
|
||||
expect(assigns(:can_passer_en_construction_was)).to eq(true)
|
||||
expect(assigns(:can_passer_en_construction_is)).to eq(true)
|
||||
expect(response.body).not_to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -868,8 +921,8 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
context 'classic error' do
|
||||
before do
|
||||
expect_any_instance_of(Dossier).to receive(:save).and_return(false)
|
||||
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||
allow_any_instance_of(Dossier).to receive(:save).and_return(false)
|
||||
allow_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||
[message: 'nop', inner_error: double(base: first_champ)]
|
||||
)
|
||||
subject
|
||||
|
|
|
@ -43,6 +43,8 @@ end
|
|||
|
||||
describe Logic::GreaterThanEq do
|
||||
include Logic
|
||||
let(:champ) { create(:champ_integer_number, value: nil) }
|
||||
|
||||
it 'computes' do
|
||||
expect(greater_than_eq(constant(0), constant(1)).compute).to be(false)
|
||||
expect(greater_than_eq(constant(1), constant(1)).compute).to be(true)
|
||||
|
|
|
@ -347,306 +347,417 @@ describe ProcedureRevision do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#compare' do
|
||||
describe '#compare_types_de_champ' do
|
||||
include Logic
|
||||
|
||||
let(:first_tdc) { draft.types_de_champ_public.first }
|
||||
let(:second_tdc) { draft.types_de_champ_public.second }
|
||||
let(:new_draft) { procedure.create_new_revision }
|
||||
subject { procedure.active_revision.compare_types_de_champ(new_draft.reload).map(&:to_h) }
|
||||
|
||||
subject { procedure.active_revision.compare(new_draft.reload).map(&:to_h) }
|
||||
describe 'when tdcs changes' do
|
||||
let(:first_tdc) { draft.types_de_champ_public.first }
|
||||
let(:second_tdc) { draft.types_de_champ_public.second }
|
||||
|
||||
context 'with a procedure with 2 tdcs' do
|
||||
let(:procedure) do
|
||||
create(:procedure, types_de_champ_public: [
|
||||
{ type: :integer_number, libelle: 'l1' },
|
||||
{ type: :text, libelle: 'l2' }
|
||||
])
|
||||
context 'with a procedure with 2 tdcs' do
|
||||
let(:procedure) do
|
||||
create(:procedure, types_de_champ_public: [
|
||||
{ type: :integer_number, libelle: 'l1' },
|
||||
{ type: :text, libelle: 'l2' }
|
||||
])
|
||||
end
|
||||
|
||||
context 'when a condition is added' do
|
||||
before do
|
||||
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
|
||||
second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
attribute: :condition,
|
||||
from: nil,
|
||||
label: "l2",
|
||||
op: :update,
|
||||
private: false,
|
||||
stable_id: second_tdc.stable_id,
|
||||
to: "(l1 == 3)"
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a condition is removed' do
|
||||
before do
|
||||
second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
|
||||
draft.reload
|
||||
|
||||
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
|
||||
second.update(condition: nil)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
attribute: :condition,
|
||||
from: "(l1 == 2)",
|
||||
label: "l2",
|
||||
op: :update,
|
||||
private: false,
|
||||
stable_id: second_tdc.stable_id,
|
||||
to: nil
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a condition is changed' do
|
||||
before do
|
||||
second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
|
||||
draft.reload
|
||||
|
||||
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
|
||||
second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
attribute: :condition,
|
||||
from: "(l1 == 2)",
|
||||
label: "l2",
|
||||
op: :update,
|
||||
private: false,
|
||||
stable_id: second_tdc.stable_id,
|
||||
to: "(l1 == 3)"
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a condition is added' do
|
||||
before do
|
||||
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
|
||||
second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
|
||||
context 'when a type de champ is added' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
let(:new_tdc) do
|
||||
new_draft.add_type_de_champ(
|
||||
type_champ: TypeDeChamp.type_champs.fetch(:text),
|
||||
libelle: "Un champ text"
|
||||
)
|
||||
end
|
||||
|
||||
before { new_tdc }
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
attribute: :condition,
|
||||
from: nil,
|
||||
label: "l2",
|
||||
op: :update,
|
||||
op: :add,
|
||||
label: "Un champ text",
|
||||
private: false,
|
||||
stable_id: second_tdc.stable_id,
|
||||
to: "(l1 == 3)"
|
||||
mandatory: false,
|
||||
stable_id: new_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a condition is removed' do
|
||||
before do
|
||||
second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
|
||||
draft.reload
|
||||
context 'when a type de champ is changed' do
|
||||
context 'when libelle, description, and mandatory are changed' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ) }
|
||||
|
||||
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
|
||||
second.update(condition: nil)
|
||||
before do
|
||||
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
|
||||
|
||||
updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :libelle,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
from: first_tdc.libelle,
|
||||
to: "modifier le libelle",
|
||||
stable_id: first_tdc.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :description,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
from: first_tdc.description,
|
||||
to: "une description",
|
||||
stable_id: first_tdc.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :mandatory,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
from: false,
|
||||
to: true,
|
||||
stable_id: first_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) }
|
||||
|
||||
before do
|
||||
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
|
||||
|
||||
updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
|
||||
end
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :collapsible_explanation_enabled,
|
||||
label: first_tdc.libelle,
|
||||
private: first_tdc.private?,
|
||||
from: false,
|
||||
to: true,
|
||||
stable_id: first_tdc.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :collapsible_explanation_text,
|
||||
label: first_tdc.libelle,
|
||||
private: first_tdc.private?,
|
||||
from: nil,
|
||||
to: 'afficher au clique',
|
||||
stable_id: first_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a type de champ is moved' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) }
|
||||
let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second }
|
||||
let(:new_draft_third_tdc) { new_draft.types_de_champ_public.third }
|
||||
|
||||
before do
|
||||
new_draft_second_tdc
|
||||
new_draft_third_tdc
|
||||
new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
attribute: :condition,
|
||||
from: "(l1 == 2)",
|
||||
label: "l2",
|
||||
op: :update,
|
||||
op: :move,
|
||||
label: new_draft_third_tdc.libelle,
|
||||
private: false,
|
||||
stable_id: second_tdc.stable_id,
|
||||
to: nil
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a condition is changed' do
|
||||
before do
|
||||
second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
|
||||
draft.reload
|
||||
|
||||
second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
|
||||
second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
from: 2,
|
||||
to: 1,
|
||||
stable_id: new_draft_third_tdc.stable_id
|
||||
},
|
||||
{
|
||||
attribute: :condition,
|
||||
from: "(l1 == 2)",
|
||||
label: "l2",
|
||||
op: :update,
|
||||
op: :move,
|
||||
label: new_draft_second_tdc.libelle,
|
||||
private: false,
|
||||
stable_id: second_tdc.stable_id,
|
||||
to: "(l1 == 3)"
|
||||
from: 1,
|
||||
to: 2,
|
||||
stable_id: new_draft_second_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a type de champ is added' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
let(:new_tdc) do
|
||||
new_draft.add_type_de_champ(
|
||||
type_champ: TypeDeChamp.type_champs.fetch(:text),
|
||||
libelle: "Un champ text"
|
||||
)
|
||||
end
|
||||
|
||||
before { new_tdc }
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :add,
|
||||
label: "Un champ text",
|
||||
private: false,
|
||||
mandatory: false,
|
||||
stable_id: new_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a type de champ is changed' do
|
||||
context 'when libelle, description, and mandatory are changed' do
|
||||
context 'when a type de champ is removed' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ) }
|
||||
|
||||
before do
|
||||
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
|
||||
|
||||
updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
|
||||
new_draft.remove_type_de_champ(first_tdc.stable_id)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :libelle,
|
||||
op: :remove,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
from: first_tdc.libelle,
|
||||
to: "modifier le libelle",
|
||||
stable_id: first_tdc.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :description,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
from: first_tdc.description,
|
||||
to: "une description",
|
||||
stable_id: first_tdc.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :mandatory,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
from: false,
|
||||
to: true,
|
||||
stable_id: first_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) }
|
||||
context 'when a child type de champ is transformed into a drop_down_list' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
|
||||
|
||||
before do
|
||||
updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
|
||||
|
||||
updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
|
||||
child = new_draft.children_of(new_draft.types_de_champ_public.last).first
|
||||
new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :drop_down_list, drop_down_options: ['one', 'two'])
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :collapsible_explanation_enabled,
|
||||
label: first_tdc.libelle,
|
||||
private: first_tdc.private?,
|
||||
from: false,
|
||||
to: true,
|
||||
stable_id: first_tdc.stable_id
|
||||
attribute: :type_champ,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: "text",
|
||||
to: "drop_down_list",
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :collapsible_explanation_text,
|
||||
label: first_tdc.libelle,
|
||||
private: first_tdc.private?,
|
||||
from: nil,
|
||||
to: 'afficher au clique',
|
||||
stable_id: first_tdc.stable_id
|
||||
attribute: :drop_down_options,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: [],
|
||||
to: ["one", "two"],
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a child type de champ is transformed into a map' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
|
||||
|
||||
before do
|
||||
child = new_draft.children_of(new_draft.types_de_champ_public.last).first
|
||||
new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :carte, options: { cadastres: true, znieff: true })
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :type_champ,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: "text",
|
||||
to: "carte",
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :carte_layers,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: [],
|
||||
to: [:cadastres, :znieff],
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a type de champ is moved' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) }
|
||||
let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second }
|
||||
let(:new_draft_third_tdc) { new_draft.types_de_champ_public.third }
|
||||
describe 'compare_ineligibilite_rules' do
|
||||
include Logic
|
||||
let(:new_draft) { procedure.create_new_revision }
|
||||
subject { procedure.active_revision.compare_ineligibilite_rules(new_draft.reload) }
|
||||
|
||||
before do
|
||||
new_draft_second_tdc
|
||||
new_draft_third_tdc
|
||||
new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2)
|
||||
context 'when ineligibilite_rules changes' do
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
|
||||
let(:types_de_champ_public) { [{ type: :yes_no }] }
|
||||
let(:yes_no_tdc) { new_draft.types_de_champ_public.first }
|
||||
|
||||
context 'when nothing changed' do
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :move,
|
||||
label: new_draft_third_tdc.libelle,
|
||||
private: false,
|
||||
from: 2,
|
||||
to: 1,
|
||||
stable_id: new_draft_third_tdc.stable_id
|
||||
},
|
||||
{
|
||||
op: :move,
|
||||
label: new_draft_second_tdc.libelle,
|
||||
private: false,
|
||||
from: 1,
|
||||
to: 2,
|
||||
stable_id: new_draft_second_tdc.stable_id
|
||||
}
|
||||
])
|
||||
context 'when ineligibilite_rules added' do
|
||||
before do
|
||||
new_draft.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
end
|
||||
|
||||
it { is_expected.to include(an_instance_of(ProcedureRevisionChange::AddEligibiliteRuleChange)) }
|
||||
end
|
||||
|
||||
context 'when ineligibilite_rules removed' do
|
||||
before do
|
||||
procedure.published_revision.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
end
|
||||
|
||||
it { is_expected.to include(an_instance_of(ProcedureRevisionChange::RemoveEligibiliteRuleChange)) }
|
||||
end
|
||||
|
||||
context 'when ineligibilite_rules changed' do
|
||||
before do
|
||||
procedure.published_revision.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
|
||||
new_draft.update!(ineligibilite_rules: ds_and([
|
||||
ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)),
|
||||
empty_operator(empty, empty)
|
||||
]))
|
||||
end
|
||||
|
||||
it { is_expected.to include(an_instance_of(ProcedureRevisionChange::UpdateEligibiliteRuleChange)) }
|
||||
end
|
||||
|
||||
context 'when when ineligibilite_enabled changes from false to true' do
|
||||
before do
|
||||
procedure.published_revision.update!(ineligibilite_enabled: false, ineligibilite_message: :required)
|
||||
new_draft.update!(ineligibilite_enabled: true, ineligibilite_message: :required)
|
||||
end
|
||||
|
||||
it { is_expected.to include(an_instance_of(ProcedureRevisionChange::EligibiliteEnabledChange)) }
|
||||
end
|
||||
|
||||
context 'when ineligibilite_enabled changes from true to false' do
|
||||
before do
|
||||
procedure.published_revision.update!(ineligibilite_enabled: true, ineligibilite_message: :required)
|
||||
new_draft.update!(ineligibilite_enabled: false, ineligibilite_message: :required)
|
||||
end
|
||||
|
||||
it { is_expected.to include(an_instance_of(ProcedureRevisionChange::EligibiliteDisabledChange)) }
|
||||
end
|
||||
|
||||
context 'when ineligibilite_message changes' do
|
||||
before do
|
||||
procedure.published_revision.update!(ineligibilite_message: :a)
|
||||
new_draft.update!(ineligibilite_message: :b)
|
||||
end
|
||||
|
||||
it { is_expected.to include(an_instance_of(ProcedureRevisionChange::UpdateEligibiliteMessageChange)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a type de champ is removed' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ) }
|
||||
|
||||
before do
|
||||
new_draft.remove_type_de_champ(first_tdc.stable_id)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :remove,
|
||||
label: first_tdc.libelle,
|
||||
private: false,
|
||||
stable_id: first_tdc.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
describe 'ineligibilite_rules_are_valid?' do
|
||||
include Logic
|
||||
let(:procedure) { create(:procedure) }
|
||||
let(:draft_revision) { procedure.draft_revision }
|
||||
let(:ineligibilite_message) { 'ok' }
|
||||
let(:ineligibilite_enabled) { true }
|
||||
before do
|
||||
procedure.draft_revision.update(ineligibilite_rules:, ineligibilite_message:, ineligibilite_enabled:)
|
||||
end
|
||||
|
||||
context 'when a child type de champ is transformed into a drop_down_list' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
|
||||
|
||||
before do
|
||||
child = new_draft.children_of(new_draft.types_de_champ_public.last).first
|
||||
new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :drop_down_list, drop_down_options: ['one', 'two'])
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :type_champ,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: "text",
|
||||
to: "drop_down_list",
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :drop_down_options,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: [],
|
||||
to: ["one", "two"],
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
}
|
||||
])
|
||||
context 'when ineligibilite_rules are valid' do
|
||||
let(:ineligibilite_rules) { ds_eq(constant(true), constant(true)) }
|
||||
it 'is valid' do
|
||||
expect(draft_revision.validate(:publication)).to be_truthy
|
||||
expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a child type de champ is transformed into a map' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
|
||||
|
||||
before do
|
||||
child = new_draft.children_of(new_draft.types_de_champ_public.last).first
|
||||
new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :carte, options: { cadastres: true, znieff: true })
|
||||
context 'when ineligibilite_rules are invalid on simple champ' do
|
||||
let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
|
||||
it 'is invalid' do
|
||||
expect(draft_revision.validate(:publication)).to be_falsey
|
||||
expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
{
|
||||
op: :update,
|
||||
attribute: :type_champ,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: "text",
|
||||
to: "carte",
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
},
|
||||
{
|
||||
op: :update,
|
||||
attribute: :carte_layers,
|
||||
label: "sub type de champ",
|
||||
private: false,
|
||||
from: [],
|
||||
to: [:cadastres, :znieff],
|
||||
stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
|
||||
}
|
||||
])
|
||||
end
|
||||
context 'when ineligibilite_rules are invalid on repetition champ' do
|
||||
let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
|
||||
let(:procedure) { create(:procedure, types_de_champ_public:) }
|
||||
let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :integer_number }] }] }
|
||||
let(:tdc_number) { draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'integer_number' } }
|
||||
let(:ineligibilite_rules) do
|
||||
ds_eq(champ_value(tdc_number.stable_id), constant(true))
|
||||
end
|
||||
it 'is invalid' do
|
||||
expect(draft_revision.validate(:publication)).to be_falsey
|
||||
expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -372,12 +372,12 @@ describe Procedure do
|
|||
]
|
||||
end
|
||||
let(:types_de_champ_private) { [] }
|
||||
let(:invalid_repetition_error_message) { 'Le champ « Enfants » doit comporter au moins un champ répétable' }
|
||||
let(:invalid_drop_down_error_message) { 'Le champ « Civilité » doit comporter au moins un choix sélectionnable' }
|
||||
let(:invalid_repetition_error_message) { "doit comporter au moins un champ répétable" }
|
||||
let(:invalid_drop_down_error_message) { "doit comporter au moins un choix sélectionnable" }
|
||||
|
||||
it 'validates that no repetition type de champ is empty' do
|
||||
procedure.validate(:publication)
|
||||
expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).to include(invalid_repetition_error_message)
|
||||
expect(procedure.errors.messages_for(:draft_types_de_champ_public)).to include(invalid_repetition_error_message)
|
||||
|
||||
new_draft = procedure.draft_revision
|
||||
repetition = procedure.draft_revision.types_de_champ_public.find(&:repetition?)
|
||||
|
@ -385,17 +385,17 @@ describe Procedure do
|
|||
new_draft.revision_types_de_champ.create(type_de_champ: create(:type_de_champ), position: 0, parent: parent_coordinate)
|
||||
|
||||
procedure.validate(:publication)
|
||||
expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).not_to include(invalid_repetition_error_message)
|
||||
expect(procedure.errors.messages_for(:draft_types_de_champ_public)).not_to include(invalid_repetition_error_message)
|
||||
end
|
||||
|
||||
it 'validates that no drop-down type de champ is empty' do
|
||||
procedure.validate(:publication)
|
||||
expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).to include(invalid_drop_down_error_message)
|
||||
expect(procedure.errors.messages_for(:draft_types_de_champ_public)).to include(invalid_drop_down_error_message)
|
||||
|
||||
drop_down = procedure.draft_revision.types_de_champ_public.find(&:drop_down_list?)
|
||||
drop_down.update!(drop_down_list_value: "--title--\r\nsome value")
|
||||
procedure.reload.validate(:publication)
|
||||
expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).not_to include(invalid_drop_down_error_message)
|
||||
expect(procedure.errors.messages_for(:draft_types_de_champ_public)).not_to include(invalid_drop_down_error_message)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -408,17 +408,21 @@ describe Procedure do
|
|||
end
|
||||
let(:types_de_champ_public) { [] }
|
||||
|
||||
let(:invalid_repetition_error_message) { 'L’annotation privée « Enfants » doit comporter au moins un champ répétable' }
|
||||
let(:invalid_drop_down_error_message) { 'L’annotation privée « Civilité » doit comporter au moins un choix sélectionnable' }
|
||||
let(:invalid_repetition_error_message) { "doit comporter au moins un champ répétable" }
|
||||
let(:invalid_drop_down_error_message) { "doit comporter au moins un choix sélectionnable" }
|
||||
|
||||
it 'validates that no repetition type de champ is empty' do
|
||||
procedure.validate(:publication)
|
||||
expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to include(invalid_repetition_error_message)
|
||||
expect(procedure.errors.messages_for(:draft_types_de_champ_private)).to include(invalid_repetition_error_message)
|
||||
repetition = procedure.draft_revision.types_de_champ_private.find(&:repetition?)
|
||||
expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(repetition)
|
||||
end
|
||||
|
||||
it 'validates that no drop-down type de champ is empty' do
|
||||
procedure.validate(:publication)
|
||||
expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to include(invalid_drop_down_error_message)
|
||||
expect(procedure.errors.messages_for(:draft_types_de_champ_private)).to include(invalid_drop_down_error_message)
|
||||
drop_down = procedure.draft_revision.types_de_champ_private.find(&:drop_down_list?)
|
||||
expect(procedure.errors.to_enum.to_a.map { _1.options[:type_de_champ] }).to include(drop_down)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -441,7 +445,7 @@ describe Procedure do
|
|||
include Logic
|
||||
let(:types_de_champ_public) { [{ type: :text, libelle: 'condition', condition: ds_eq(champ_value(1), constant(2)), stable_id: 2 }] }
|
||||
let(:types_de_champ_private) { [{ type: :decimal_number, stable_id: 1 }] }
|
||||
let(:error_on_condition) { "Le champ « condition » a une logique conditionnelle invalide" }
|
||||
let(:error_on_condition) { "Le champ a une logique conditionnelle invalide" }
|
||||
|
||||
it 'validate without context' do
|
||||
procedure.validate
|
||||
|
|
45
spec/system/administrateurs/procedure_ineligibilite_spec.rb
Normal file
45
spec/system/administrateurs/procedure_ineligibilite_spec.rb
Normal 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 l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire : ")
|
||||
click_on "Ajouter un champ supportant les conditions d’inéligibilité"
|
||||
|
||||
# setup a compatible champ
|
||||
expect(page).to have_content('Champs du formulaire')
|
||||
click_on 'Ajouter un champ'
|
||||
select "Oui/Non"
|
||||
fill_in "Libellé du champ", with: "Un champ oui non"
|
||||
click_on "Revenir à l'écran de gestion"
|
||||
procedure.reload
|
||||
first_tdc = procedure.draft_revision.types_de_champ.first
|
||||
# back to procedure dashboard, explain you can set it up now
|
||||
expect(page).to have_content('À configurer')
|
||||
visit edit_admin_procedure_ineligibilite_rules_path(procedure)
|
||||
|
||||
# setup rules and stuffs
|
||||
expect(page).to have_content("Inéligibilité des dossiers")
|
||||
fill_in "Message d’inéligibilité", with: "vous n'etes pas eligible"
|
||||
find('label', text: 'Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité').click
|
||||
click_on "Ajouter une règle d’inéligibilité"
|
||||
all('select').first.select 'Un champ oui non'
|
||||
click_on 'Enregistrer'
|
||||
|
||||
# rules are setup
|
||||
wait_until { procedure.reload.draft_revision.ineligibilite_enabled == true }
|
||||
expect(procedure.draft_revision.ineligibilite_message).to eq("vous n'etes pas eligible")
|
||||
expect(procedure.draft_revision.ineligibilite_rules).to eq(ds_eq(champ_value(first_tdc.stable_id), constant(true)))
|
||||
end
|
||||
end
|
|
@ -72,8 +72,8 @@ describe 'Publishing a procedure', js: true do
|
|||
visit admin_procedure_path(procedure)
|
||||
|
||||
expect(page).to have_content('Des problèmes empêchent la publication de la démarche')
|
||||
expect(page).to have_content("« Enfants » doit comporter au moins un champ répétable")
|
||||
expect(page).to have_content("« Civilité » doit comporter au moins un choix sélectionnable")
|
||||
expect(page).to have_content("Enfants doit comporter au moins un champ répétable")
|
||||
expect(page).to have_content("Civilité doit comporter au moins un choix sélectionnable")
|
||||
|
||||
visit admin_procedure_publication_path(procedure)
|
||||
expect(find_field('procedure_path').value).to eq procedure.path
|
||||
|
@ -195,7 +195,7 @@ describe 'Publishing a procedure', js: true do
|
|||
scenario 'an error message prevents the publication' do
|
||||
visit admin_procedure_path(procedure)
|
||||
expect(page).to have_content('Des problèmes empêchent la publication des modifications')
|
||||
expect(page).to have_link('corriger', href: edit_admin_procedure_mail_template_path(procedure, Mails::InitiatedMail::SLUG))
|
||||
expect(page).to have_link(href: edit_admin_procedure_mail_template_path(procedure, Mails::InitiatedMail::SLUG))
|
||||
expect(page).to have_button('Publier les modifications', disabled: true)
|
||||
end
|
||||
end
|
||||
|
|
182
spec/system/users/dossier_ineligibilite_spec.rb
Normal file
182
spec/system/users/dossier_ineligibilite_spec.rb
Normal 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
|
|
@ -149,4 +149,17 @@ describe 'shared/dossiers/edit', type: :view do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when dossier transitions rules are computable and passer_en_construction is false' do
|
||||
let(:types_de_champ_public) { [] }
|
||||
let(:dossier) { create(:dossier, procedure:) }
|
||||
|
||||
before do
|
||||
allow(dossier).to receive(:can_passer_en_construction?).and_return(false)
|
||||
end
|
||||
|
||||
it 'renders broken transitions rules dialog' do
|
||||
expect(subject).to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue