diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 11e97ca81..89d52a61d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -81,7 +81,8 @@ jobs:
- name: Install build dependancies
# - fonts pickable by ImageMagick
# - rust for YJIT support
- run: sudo apt-get update && sudo apt-get install -y gsfonts rustc redis-server
+ # - poppler-utils for pdf previews
+ run: sudo apt-get update && sudo apt-get install -y gsfonts rustc redis-server poppler-utils
- name: Setup the app runtime and dependencies
uses: ./.github/actions/ci-setup-rails
diff --git a/Gemfile b/Gemfile
index 8d78db84d..9a9ff1600 100644
--- a/Gemfile
+++ b/Gemfile
@@ -95,6 +95,7 @@ gem 'sidekiq'
gem 'sidekiq-cron'
gem 'skylight'
gem 'spreadsheet_architect'
+gem 'string-similarity'
gem 'strong_migrations' # lint database migrations
gem 'sys-proctable'
gem 'turbo-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 51281c712..602d74ace 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -12,47 +12,47 @@ GEM
aasm (5.5.0)
concurrent-ruby (~> 1.0)
acsv (0.0.1)
- actioncable (7.0.8.3)
- actionpack (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ actioncable (7.0.8.4)
+ actionpack (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (7.0.8.3)
- actionpack (= 7.0.8.3)
- activejob (= 7.0.8.3)
- activerecord (= 7.0.8.3)
- activestorage (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ actionmailbox (7.0.8.4)
+ actionpack (= 7.0.8.4)
+ activejob (= 7.0.8.4)
+ activerecord (= 7.0.8.4)
+ activestorage (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.0.8.3)
- actionpack (= 7.0.8.3)
- actionview (= 7.0.8.3)
- activejob (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ actionmailer (7.0.8.4)
+ actionpack (= 7.0.8.4)
+ actionview (= 7.0.8.4)
+ activejob (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
- actionpack (7.0.8.3)
- actionview (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ actionpack (7.0.8.4)
+ actionview (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (7.0.8.3)
- actionpack (= 7.0.8.3)
- activerecord (= 7.0.8.3)
- activestorage (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ actiontext (7.0.8.4)
+ actionpack (= 7.0.8.4)
+ activerecord (= 7.0.8.4)
+ activestorage (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.0.8.3)
- activesupport (= 7.0.8.3)
+ actionview (7.0.8.4)
+ activesupport (= 7.0.8.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -67,26 +67,26 @@ GEM
activemodel (>= 5.2.0)
activestorage (>= 5.2.0)
activesupport (>= 5.2.0)
- activejob (7.0.8.3)
- activesupport (= 7.0.8.3)
+ activejob (7.0.8.4)
+ activesupport (= 7.0.8.4)
globalid (>= 0.3.6)
- activemodel (7.0.8.3)
- activesupport (= 7.0.8.3)
- activerecord (7.0.8.3)
- activemodel (= 7.0.8.3)
- activesupport (= 7.0.8.3)
- activestorage (7.0.8.3)
- actionpack (= 7.0.8.3)
- activejob (= 7.0.8.3)
- activerecord (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ activemodel (7.0.8.4)
+ activesupport (= 7.0.8.4)
+ activerecord (7.0.8.4)
+ activemodel (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
+ activestorage (7.0.8.4)
+ actionpack (= 7.0.8.4)
+ activejob (= 7.0.8.4)
+ activerecord (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activestorage-openstack (1.6.0)
fog-openstack (>= 1.0.9)
marcel
rails (>= 5.2.2)
- activesupport (7.0.8.3)
+ activesupport (7.0.8.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -174,7 +174,7 @@ GEM
clamav-client (3.2.0)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- concurrent-ruby (1.2.3)
+ concurrent-ruby (1.3.1)
connection_pool (2.4.1)
content_disposition (1.0.0)
crack (1.0.0)
@@ -438,15 +438,15 @@ GEM
rake
mini_magick (4.12.0)
mini_mime (1.1.5)
- mini_portile2 (2.8.6)
- minitest (5.23.0)
+ mini_portile2 (2.8.7)
+ minitest (5.23.1)
msgpack (1.7.2)
multi_json (1.15.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
net-http (0.4.1)
uri
- net-imap (0.4.11)
+ net-imap (0.4.12)
date
net-protocol
net-pop (0.1.2)
@@ -531,20 +531,20 @@ GEM
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
- rails (7.0.8.3)
- actioncable (= 7.0.8.3)
- actionmailbox (= 7.0.8.3)
- actionmailer (= 7.0.8.3)
- actionpack (= 7.0.8.3)
- actiontext (= 7.0.8.3)
- actionview (= 7.0.8.3)
- activejob (= 7.0.8.3)
- activemodel (= 7.0.8.3)
- activerecord (= 7.0.8.3)
- activestorage (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ rails (7.0.8.4)
+ actioncable (= 7.0.8.4)
+ actionmailbox (= 7.0.8.4)
+ actionmailer (= 7.0.8.4)
+ actionpack (= 7.0.8.4)
+ actiontext (= 7.0.8.4)
+ actionview (= 7.0.8.4)
+ activejob (= 7.0.8.4)
+ activemodel (= 7.0.8.4)
+ activerecord (= 7.0.8.4)
+ activestorage (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
bundler (>= 1.15.0)
- railties (= 7.0.8.3)
+ railties (= 7.0.8.4)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -567,9 +567,9 @@ GEM
rails-pg-extras (5.3.1)
rails
ruby-pg-extras (= 5.3.1)
- railties (7.0.8.3)
- actionpack (= 7.0.8.3)
- activesupport (= 7.0.8.3)
+ railties (7.0.8.4)
+ actionpack (= 7.0.8.4)
+ activesupport (= 7.0.8.4)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -765,6 +765,7 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stackprof (0.2.26)
+ string-similarity (2.1.0)
stringio (3.1.0)
strong_migrations (1.8.0)
activerecord (>= 5.2)
@@ -872,7 +873,7 @@ GEM
anyway_config (>= 1.3, < 3)
sidekiq
yabeda (~> 0.6)
- zeitwerk (2.6.14)
+ zeitwerk (2.6.15)
zip_tricks (5.6.0)
zipline (1.5.0)
actionpack (>= 6.0, < 8.0)
@@ -1013,6 +1014,7 @@ DEPENDENCIES
spring
spring-commands-rspec
stackprof
+ string-similarity
strong_migrations
sys-proctable
timecop
diff --git a/app/assets/stylesheets/conditions_component.scss b/app/assets/stylesheets/conditions_component.scss
index 055e9b4f9..59f8bf6b9 100644
--- a/app/assets/stylesheets/conditions_component.scss
+++ b/app/assets/stylesheets/conditions_component.scss
@@ -57,5 +57,14 @@ form.form > .conditionnel {
select.alert {
border-color: $dark-red;
}
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &:last-child {
+ text-align: right;
+ padding-right: 0;
+ }
}
}
diff --git a/app/components/conditions/conditions_component.rb b/app/components/conditions/conditions_component.rb
index 01f081a85..8817a1336 100644
--- a/app/components/conditions/conditions_component.rb
+++ b/app/components/conditions/conditions_component.rb
@@ -61,7 +61,7 @@ class Conditions::ConditionsComponent < ApplicationComponent
def available_targets_for_select
@source_tdcs
- .filter { |tdc| ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(tdc.type_champ) }
+ .filter(&:conditionable?)
.map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
end
diff --git a/app/components/conditions/ineligibilite_rules_component.rb b/app/components/conditions/ineligibilite_rules_component.rb
new file mode 100644
index 000000000..a12ab262e
--- /dev/null
+++ b/app/components/conditions/ineligibilite_rules_component.rb
@@ -0,0 +1,34 @@
+class Conditions::IneligibiliteRulesComponent < Conditions::ConditionsComponent
+ include Logic
+
+ def initialize(draft_revision:)
+ @draft_revision = draft_revision
+ @published_revision = draft_revision.procedure.published_revision
+ @condition = draft_revision.ineligibilite_rules
+ @source_tdcs = draft_revision.types_de_champ_for(scope: :public)
+ end
+
+ def pending_changes?
+ return false if !@published_revision
+
+ !@published_revision.compare_ineligibilite_rules(@draft_revision).empty?
+ end
+
+ private
+
+ def input_prefix
+ 'procedure_revision[condition_form]'
+ end
+
+ def input_id_for(name, row_index)
+ "#{@draft_revision.id}-#{name}-#{row_index}"
+ end
+
+ def delete_condition_path(row_index)
+ delete_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id, row_index:)
+ end
+
+ def add_condition_path
+ add_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id)
+ end
+end
diff --git a/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.fr.yml b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.fr.yml
new file mode 100644
index 000000000..b646c3019
--- /dev/null
+++ b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.fr.yml
@@ -0,0 +1,6 @@
+---
+fr:
+ display_if: Bloquer si
+ select: Sélectionner
+ add_condition: Ajouter une règle d’inéligibilité
+ remove_a_row: Supprimer une règle
diff --git a/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.html.haml b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.html.haml
new file mode 100644
index 000000000..313618167
--- /dev/null
+++ b/app/components/conditions/ineligibilite_rules_component/ineligibilite_rules_component.html.haml
@@ -0,0 +1,49 @@
+%div{ id: dom_id(@draft_revision, :ineligibilite_rules) }
+ = render Procedure::PendingRepublishComponent.new(procedure: @draft_revision.procedure, render_if: pending_changes?)
+ = render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs)
+ .fr-fieldset
+ = form_for(@draft_revision, url: change_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), html: { id: 'ineligibilite_form', class: 'width-100' }) do |f|
+ .fr-fieldset__element
+ .fr-toggle.fr-toggle--label-left
+ = f.check_box :ineligibilite_enabled, class: 'fr-toggle__input', data: @opt
+ = f.label :ineligibilite_enabled, "Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité", data: { 'fr-checked-label': "Activé", 'fr-unchecked-label': "Désactivé" }, class: 'fr-toggle__label'
+
+ .fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :ineligibilite_message, input_type: :text_area, opts: {rows: 5})
+
+ .fr-mx-1w.fr-label.fr-py-0.fr-mb-1w.fr-mt-2w
+ Conditions d’inéligibilité
+ %span.fr-hint-text Vous pouvez utiliser une ou plusieurs condtions pour bloquer le dépot.
+ .fr-fieldset__element
+ = form_tag admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), method: :patch, data: { turbo: true, controller: 'autosave' }, class: 'form width-100' do
+ .conditionnel.width-100
+ %table.condition-table
+ - if rows.size > 0
+ %thead
+ %tr
+ %th.fr-pt-0.far-left
+ %th.fr-pt-0.target Champ Cible
+ %th.fr-pt-0.operator Opérateur
+ %th.fr-pt-0.value Valeur
+ %th.fr-pt-0.delete-column
+ %tbody
+ - rows.each.with_index do |(targeted_champ, operator_name, value), row_index|
+ %tr
+ %td.far-left= far_left_tag(row_index)
+ %td.target= left_operand_tag(targeted_champ, row_index)
+ %td.operator= operator_tag(operator_name, targeted_champ, row_index)
+ %td.value= right_operand_tag(targeted_champ, value, row_index, operator_name)
+ %td.delete-column= delete_condition_tag(row_index)
+ %tfoot
+ %tr
+ %td.text-right{ colspan: 5 }= add_condition_tag
+
+ .padded-fixed-footer
+ .fixed-footer
+ .fr-container
+ .fr-grid-row.fr-col-offset-md-2.fr-col-md-8
+ .fr-col-12
+ %ul.fr-btns-group.fr-btns-group--inline-md
+ %li
+ = link_to "Annuler et revenir à l'écran de gestion", admin_procedure_path(id: @draft_revision.procedure), class: 'fr-btn fr-btn--secondary', data: { confirm: 'Si vous avez fait des modifications elles ne seront pas sauvegardées.'}
+ %li
+ = button_tag "Enregistrer", class: "fr-btn", form: 'ineligibilite_form'
diff --git a/app/components/dossiers/edit_footer_component.rb b/app/components/dossiers/edit_footer_component.rb
index fca7fab45..5f5bb8980 100644
--- a/app/components/dossiers/edit_footer_component.rb
+++ b/app/components/dossiers/edit_footer_component.rb
@@ -1,4 +1,6 @@
class Dossiers::EditFooterComponent < ApplicationComponent
+ delegate :can_passer_en_construction?, to: :@dossier
+
def initialize(dossier:, annotation:)
@dossier = dossier
@annotation = annotation
@@ -14,20 +16,29 @@ class Dossiers::EditFooterComponent < ApplicationComponent
@annotation.present?
end
+ def disabled_submit_buttons_options
+ {
+ class: 'fr-text--sm fr-mb-0 fr-mr-2w',
+ data: { 'fr-opened': "true" },
+ aria: { controls: 'modal-eligibilite-rules-dialog' }
+ }
+ end
+
def submit_draft_button_options
{
class: 'fr-btn fr-btn--sm',
- disabled: !owner?,
+ disabled: !owner? || !can_passer_en_construction?,
method: :post,
- data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' }
+ data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server }
}
end
def submit_en_construction_button_options
{
class: 'fr-btn fr-btn--sm',
+ disabled: !can_passer_en_construction?,
method: :post,
- data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' },
+ data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server },
form: { id: "form-submit-en-construction" }
}
end
diff --git a/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml b/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml
index 098e6ec0b..b6de7d121 100644
--- a/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml
+++ b/app/components/dossiers/edit_footer_component/edit_footer_component.en.yml
@@ -2,5 +2,6 @@
en:
submit: Submit the file
submit_changes: Submit file changes
+ submit_disabled: File submission disabled
submitting: Submitting…
invite_notice: You are invited to make amendments to this file but only the owner themselves can submit it.
diff --git a/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml b/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml
index 33937aed6..8ffd062db 100644
--- a/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml
+++ b/app/components/dossiers/edit_footer_component/edit_footer_component.fr.yml
@@ -2,5 +2,6 @@
fr:
submit: Déposer le dossier
submit_changes: Déposer les modifications
+ submit_disabled: Pourquoi je ne peux pas déposer mon dossier ?
submitting: Envoi en cours…
invite_notice: En tant qu’invité, vous pouvez remplir ce formulaire – mais le titulaire du dossier doit le déposer lui-même.
diff --git a/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml b/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml
index 77540bd16..2f0f59b2b 100644
--- a/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml
+++ b/app/components/dossiers/edit_footer_component/edit_footer_component.html.haml
@@ -3,8 +3,13 @@
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
- if !annotation? && @dossier.can_transition_to_en_construction?
+ - if !can_passer_en_construction?
+ = link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
= button_to t('.submit'), brouillon_dossier_url(@dossier), submit_draft_button_options
- - elsif @dossier.forked_with_changes?
+
+ - if @dossier.forked_with_changes?
+ - if !can_passer_en_construction?
+ = link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
= button_to t('.submit_changes'), modifier_dossier_url(@dossier.editing_fork_origin), submit_en_construction_button_options
diff --git a/app/components/dossiers/errors_full_messages_component.rb b/app/components/dossiers/errors_full_messages_component.rb
index fd8bafd94..207170e8c 100644
--- a/app/components/dossiers/errors_full_messages_component.rb
+++ b/app/components/dossiers/errors_full_messages_component.rb
@@ -3,17 +3,15 @@
class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
ErrorDescriptor = Data.define(:anchor, :label, :error_message)
- def initialize(dossier:, errors:)
+ def initialize(dossier:)
@dossier = dossier
- @errors = errors
end
def dedup_and_partitioned_errors
- formated_errors = @errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
+ @dossier.errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
.to_a # but enum.to_a gives back an array
.uniq { |error| [error.inner_error.base] } # dedup cumulated errors from dossier.champs, dossier.champs_public, dossier.champs_private which run the validator one time per association
.map { |error| to_error_descriptor(error) }
- yield(Array(formated_errors[0..2]), Array(formated_errors[3..]))
end
def to_error_descriptor(error)
@@ -27,6 +25,6 @@ class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
end
def render?
- !@errors.empty?
+ !@dossier.errors.empty?
end
end
diff --git a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml
index 3fab8164d..0a595e80a 100644
--- a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml
+++ b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.en.yml
@@ -5,4 +5,3 @@ en:
Your file has 1 error. Fix-it to continue :
other: |
Your file has %{count} errors. Fix-them to continue :
- see_more: Show all errors
diff --git a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml
index 1fd0e7f8c..3d94f636f 100644
--- a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml
+++ b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.fr.yml
@@ -5,4 +5,3 @@ fr:
Votre dossier contient 1 champ en erreur. Corrigez-la pour poursuivre :
other: |
Votre dossier contient %{count} champs en erreurs. Corrigez-les pour poursuivre :
- see_more: Afficher toutes les erreurs
diff --git a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml
index 58d76cb56..ada4150b5 100644
--- a/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml
+++ b/app/components/dossiers/errors_full_messages_component/errors_full_messages_component.html.haml
@@ -1,15 +1,4 @@
.fr-alert.fr-alert--error.fr-mb-3w{ role: "alertdialog" }
- - dedup_and_partitioned_errors do |head, tail|
- %p#sumup-errors= t('.sumup_html', count: head.size + tail.size, url: head.first.anchor)
- %ul.fr-mb-0#head-errors
- - head.each do |error_descriptor|
- %li
- = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
- = error_descriptor.error_message
- - if tail.size > 0
- %button{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline" }= t('.see_more')
- %ul#tail-errors.fr-collapse.fr-mt-0
- - tail.each do |error_descriptor|
- %li
- = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
- = "(#{error_descriptor.error_message})"
+ - if dedup_and_partitioned_errors.size > 0
+ %p#sumup-errors= t('.sumup_html', count: dedup_and_partitioned_errors.size, url: dedup_and_partitioned_errors.first.anchor)
+ = render ExpandableErrorList.new(errors: dedup_and_partitioned_errors)
diff --git a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml
index fbb499483..fd29214f2 100644
--- a/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml
+++ b/app/components/dossiers/export_dropdown_component/export_dropdown_component.html.haml
@@ -15,12 +15,12 @@
= link_to download_export_path(export_format: format), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
= t(".everything_#{format}_html")
- - if export_templates.present?
- - export_templates.each do |export_template|
+ - if @procedure.feature_enabled?(:export_template)
+ - if export_templates.present?
+ - export_templates.each do |export_template|
+ - menu.with_item do
+ = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
+ = "Exporter à partir du modèle #{export_template.name}"
- menu.with_item do
- = link_to download_export_path(export_template_id: export_template.id), role: 'menuitem', data: { turbo_method: :post, turbo: true } do
- = "Exporter à partir du modèle #{export_template.name}"
- - if feature_enabled?(:export_template)
- - menu.with_item do
- = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do
- Ajouter un modèle d'export
+ = link_to new_instructeur_export_template_path(procedure_id: params[:procedure_id]), role: 'menuitem' do
+ Ajouter un modèle d'export
diff --git a/app/components/dossiers/export_link_component.rb b/app/components/dossiers/export_link_component.rb
index 0fe2967aa..647d08a4b 100644
--- a/app/components/dossiers/export_link_component.rb
+++ b/app/components/dossiers/export_link_component.rb
@@ -11,9 +11,10 @@ class Dossiers::ExportLinkComponent < ApplicationComponent
@export_url = export_url
end
- def download_export_path(export_format:, statut:, no_progress_notification: nil)
+ def download_export_path(export_format:, statut:, export_template_id: nil, no_progress_notification: nil)
@export_url.call(@procedure,
export_format: export_format,
+ export_template_id:,
statut: statut,
no_progress_notification: no_progress_notification)
end
diff --git a/app/components/dossiers/export_link_component/export_link_component.html.haml b/app/components/dossiers/export_link_component/export_link_component.html.haml
index 0ed8d34a2..32d631e33 100644
--- a/app/components/dossiers/export_link_component/export_link_component.html.haml
+++ b/app/components/dossiers/export_link_component/export_link_component.html.haml
@@ -7,6 +7,9 @@
= export_title(export)
%span.fr-text-mention--grey.fr-mb-1w
= time_info(export)
+ - if export.export_template
+ %span.fr-tag.fr-tag--sm.fr-ml-1w
+ = export.export_template.name
.fr-ml-auto
= badge(export)
@@ -14,4 +17,4 @@
= export_button(export)
- if export.failed?
- = button_to refresh_button_options(export)[:title], download_export_path(export_format: export.format, statut: export.statut), refresh_button_options(export)
+ = button_to refresh_button_options(export)[:title], download_export_path(export_template_id: export.export_template&.id, export_format: export.format, statut: export.statut), refresh_button_options(export)
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component.rb b/app/components/dossiers/invalid_ineligibilite_rules_component.rb
new file mode 100644
index 000000000..526bdbc94
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component.rb
@@ -0,0 +1,16 @@
+class Dossiers::InvalidIneligibiliteRulesComponent < ApplicationComponent
+ delegate :can_passer_en_construction?, to: :@dossier
+
+ def initialize(dossier:)
+ @dossier = dossier
+ @revision = dossier.revision
+ end
+
+ def render?
+ !can_passer_en_construction?
+ end
+
+ def error_message
+ @dossier.revision.ineligibilite_message
+ end
+end
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml
new file mode 100644
index 000000000..1a377763c
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml
@@ -0,0 +1,6 @@
+fr:
+ modal:
+ title: "Your file does not match submission criteria"
+ close: "Close"
+ close_alt: "Close this modal"
+ body: "The procedure « %{procedure_libelle} » have submission criteria, unfortunately your file does not match them. You can not submit your file"
\ No newline at end of file
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml
new file mode 100644
index 000000000..d191f03d4
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml
@@ -0,0 +1,5 @@
+fr:
+ modal:
+ title: "Vous ne pouvez pas déposer votre dossier"
+ close: "Fermer"
+ close_alt: "Fermer la fenêtre modale"
\ No newline at end of file
diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml
new file mode 100644
index 000000000..dd39925cd
--- /dev/null
+++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml
@@ -0,0 +1,16 @@
+%div{ id: dom_id(@dossier, :ineligibilite_rules_broken), data: { controller: 'ineligibilite-rules-match', turbo_force: :server } }
+ %button.fr-sr-only{ aria: {controls: 'modal-eligibilite-rules-dialog' }, data: {'fr-opened': "false" } }
+ show modal
+
+ %dialog.fr-modal{ "aria-labelledby" => "fr-modal-title-modal-1", role: "dialog", id: 'modal-eligibilite-rules-dialog', data: { 'ineligibilite-rules-match-target' => 'dialog' } }
+ .fr-container.fr-container--fluid.fr-container-md
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-12.fr-col-md-8.fr-col-lg-6
+ .fr-modal__body
+ .fr-modal__header
+ %button.fr-btn--close.fr-btn{ aria: { controls: 'modal-eligibilite-rules-dialog' }, title: t('.modal.close_alt') }= t('.modal.close')
+ .fr-modal__content
+ %h1#fr-modal-title-modal-1.fr-modal__title
+ %span.fr-icon-arrow-right-line.fr-icon--lg>
+ = t('.modal.title')
+ %p= error_message
diff --git a/app/components/dsfr/input_component.rb b/app/components/dsfr/input_component.rb
index 3ee07149b..367ed74b0 100644
--- a/app/components/dsfr/input_component.rb
+++ b/app/components/dsfr/input_component.rb
@@ -32,7 +32,7 @@ class Dsfr::InputComponent < ApplicationComponent
}.merge(input_group_error_class_names))
}
if email?
- opts[:data] = { controller: 'email-input' }
+ opts[:data] = { controller: 'email-input', email_input_url_value: show_email_suggestions_path }
end
opts
end
diff --git a/app/components/dsfr/toggle_component.rb b/app/components/dsfr/toggle_component.rb
index 20c328e9b..f0a4114b0 100644
--- a/app/components/dsfr/toggle_component.rb
+++ b/app/components/dsfr/toggle_component.rb
@@ -1,5 +1,5 @@
class Dsfr::ToggleComponent < ApplicationComponent
- def initialize(form:, target:, title:, disabled: nil, hint: nil, toggle_labels: { checked: 'Activé', unchecked: 'Désactivé' }, opt: nil)
+ def initialize(form:, target:, title:, disabled: nil, hint: nil, toggle_labels: { checked: 'Activé', unchecked: 'Désactivé' }, opt: nil, extra_class_names: nil)
@form = form
@target = target
@title = title
@@ -7,7 +7,8 @@ class Dsfr::ToggleComponent < ApplicationComponent
@disabled = disabled
@toggle_labels = toggle_labels
@opt = opt
+ @extra_class_names = extra_class_names
end
- attr_reader :toggle_labels
+ attr_reader :toggle_labels, :extra_class_names
end
diff --git a/app/components/dsfr/toggle_component/toggle_component.html.haml b/app/components/dsfr/toggle_component/toggle_component.html.haml
index 7769a3724..18bde573d 100644
--- a/app/components/dsfr/toggle_component/toggle_component.html.haml
+++ b/app/components/dsfr/toggle_component/toggle_component.html.haml
@@ -1,4 +1,4 @@
-.fr-toggle.fr-toggle--label-left
+%div{ class: "fr-toggle fr-toggle--label-left #{extra_class_names}" }
= @form.check_box @target, class: 'fr-toggle__input', disabled: @disabled,
data: @opt
= @form.label @target,
diff --git a/app/components/expandable_error_list.rb b/app/components/expandable_error_list.rb
new file mode 100644
index 000000000..43d5c9215
--- /dev/null
+++ b/app/components/expandable_error_list.rb
@@ -0,0 +1,9 @@
+class ExpandableErrorList < ApplicationComponent
+ def initialize(errors:)
+ @errors = errors
+ end
+
+ def splitted_errors
+ yield(Array(@errors[0..2]), Array(@errors[3..]))
+ end
+end
diff --git a/app/components/expandable_error_list/expandable_error_list.html.en.yml b/app/components/expandable_error_list/expandable_error_list.html.en.yml
new file mode 100644
index 000000000..b21ee7d8a
--- /dev/null
+++ b/app/components/expandable_error_list/expandable_error_list.html.en.yml
@@ -0,0 +1,3 @@
+---
+en:
+ see_more: Show all errors
diff --git a/app/components/expandable_error_list/expandable_error_list.html.fr.yml b/app/components/expandable_error_list/expandable_error_list.html.fr.yml
new file mode 100644
index 000000000..755d13886
--- /dev/null
+++ b/app/components/expandable_error_list/expandable_error_list.html.fr.yml
@@ -0,0 +1,3 @@
+---
+fr:
+ see_more: Afficher toutes les erreurs
diff --git a/app/components/expandable_error_list/expandable_error_list.html.haml b/app/components/expandable_error_list/expandable_error_list.html.haml
new file mode 100644
index 000000000..1ab5221e5
--- /dev/null
+++ b/app/components/expandable_error_list/expandable_error_list.html.haml
@@ -0,0 +1,14 @@
+- splitted_errors do |head, tail|
+ %ul#head-errors.fr-mb-0
+ - head.each do |error_descriptor|
+ %li
+ = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
+ = error_descriptor.error_message
+
+ - if tail.size > 0
+ %button.fr-mt-0.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "" }= t('see_more')
+ %ul#tail-errors.fr-collapse.fr-mt-0
+ - tail.each do |error_descriptor|
+ %li
+ = link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
+ = error_descriptor.error_message
diff --git a/app/components/procedure/card/champs_component.rb b/app/components/procedure/card/champs_component.rb
index d45b2f666..38604c831 100644
--- a/app/components/procedure/card/champs_component.rb
+++ b/app/components/procedure/card/champs_component.rb
@@ -7,9 +7,6 @@ class Procedure::Card::ChampsComponent < ApplicationComponent
private
def error_messages
- [
- @procedure.errors.messages_for(:draft_types_de_champ_public),
- @procedure.errors.messages_for(:draft_revision)
- ].flatten.to_sentence
+ @procedure.errors.messages_for(:draft_types_de_champ_public).to_sentence
end
end
diff --git a/app/components/procedure/card/ineligibilite_dossier_component.rb b/app/components/procedure/card/ineligibilite_dossier_component.rb
new file mode 100644
index 000000000..b1d371708
--- /dev/null
+++ b/app/components/procedure/card/ineligibilite_dossier_component.rb
@@ -0,0 +1,19 @@
+class Procedure::Card::IneligibiliteDossierComponent < ApplicationComponent
+ def initialize(procedure:)
+ @procedure = procedure
+ end
+
+ def ready?
+ @procedure.draft_revision
+ .conditionable_types_de_champ
+ .present? && @procedure.draft_revision.ineligibilite_enabled
+ end
+
+ def error?
+ !@procedure.draft_revision.validate(:ineligibilite_rules_editor)
+ end
+
+ def completed?
+ @procedure.draft_revision.ineligibilite_enabled
+ end
+end
diff --git a/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.fr.yml b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.fr.yml
new file mode 100644
index 000000000..6e78d7da6
--- /dev/null
+++ b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.fr.yml
@@ -0,0 +1,8 @@
+---
+fr:
+ title: Inéligibilité des dossiers
+ state:
+ pending: Désactivé
+ ready: À configurer
+ completed: Activé
+ subtitle: Gérez vos conditions d’inéligibilité en fonction des champs du formulaire
diff --git a/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.html.haml b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.html.haml
new file mode 100644
index 000000000..aeced88e6
--- /dev/null
+++ b/app/components/procedure/card/ineligibilite_dossier_component/ineligibilite_dossier_component.html.haml
@@ -0,0 +1,13 @@
+.fr-col-6.fr-col-md-4.fr-col-lg-3
+ = link_to edit_admin_procedure_ineligibilite_rules_path(@procedure), class: 'fr-tile fr-enlarge-link' do
+ .fr-tile__body.flex.column.align-center.justify-between
+ - if !ready?
+ %p.fr-badge.fr-badge= t('.state.pending')
+ - elsif error?
+ %p.fr-badge.fr-badge--error À modifier
+ - else
+ %p.fr-badge.fr-badge--success= t('.state.completed')
+ %div
+ %h3.fr-h6.fr-mt-10v= t('.title')
+ %p.fr-tile-subtitle= t('.subtitle')
+ %p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')
diff --git a/app/components/procedure/card/modifications_component.rb b/app/components/procedure/card/modifications_component.rb
deleted file mode 100644
index 35b90a624..000000000
--- a/app/components/procedure/card/modifications_component.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class Procedure::Card::ModificationsComponent < ApplicationComponent
- def initialize(procedure:)
- @procedure = procedure
- end
-
- def render?
- @procedure.revised?
- end
-end
diff --git a/app/components/procedure/card/modifications_component/modifications_component.fr.yml b/app/components/procedure/card/modifications_component/modifications_component.fr.yml
deleted file mode 100644
index 676fc64cd..000000000
--- a/app/components/procedure/card/modifications_component/modifications_component.fr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-fr:
- title:
- one: Modification du formulaire
- other: Modifications du formulaire
diff --git a/app/components/procedure/card/modifications_component/modifications_component.html.haml b/app/components/procedure/card/modifications_component/modifications_component.html.haml
deleted file mode 100644
index 9dc203c17..000000000
--- a/app/components/procedure/card/modifications_component/modifications_component.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-.fr-col-6.fr-col-md-4.fr-col-lg-3
- = link_to modifications_admin_procedure_path(@procedure), id: 'modifications', class: 'fr-tile fr-enlarge-link' do
- .fr-tile__body.flex.column.align-center.justify-between
- %p.fr-badge.fr-badge--success Activée
- %div
- .line-count.fr-my-1w
- %p.fr-tag= @procedure.revisions_count
- %h3.fr-h6
- = t('.title', count: @procedure.revisions_count)
-
- %p.fr-tile-subtitle Historique des modifications apportées au formulaire
- %p.fr-btn.fr-btn--tertiary Voir
diff --git a/app/components/procedure/errors_summary.rb b/app/components/procedure/errors_summary.rb
new file mode 100644
index 000000000..bf41ab3da
--- /dev/null
+++ b/app/components/procedure/errors_summary.rb
@@ -0,0 +1,60 @@
+class Procedure::ErrorsSummary < ApplicationComponent
+ ErrorDescriptor = Data.define(:anchor, :label, :error_message)
+
+ def initialize(procedure:, validation_context:)
+ @procedure = procedure
+ @validation_context = validation_context
+ end
+
+ def title
+ case @validation_context
+ when :types_de_champ_private_editor
+ "Les annotations privées contiennent des erreurs"
+ when :types_de_champ_public_editor
+ "Les champs formulaire contiennent des erreurs"
+ when :publication
+ if @procedure.publiee?
+ "Des problèmes empêchent la publication des modifications"
+ else
+ "Des problèmes empêchent la publication de la démarche"
+ end
+ end
+ end
+
+ def invalid?
+ @procedure.validate(@validation_context)
+ @procedure.errors.present?
+ 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))
+ when :draft_types_de_champ_private
+ tdc = error.options[:type_de_champ]
+ annotations_admin_procedure_path(@procedure, anchor: dom_id(tdc.stable_self, :editor_error))
+ when :attestation_template
+ edit_admin_procedure_attestation_template_path(@procedure)
+ when :initiated_mail, :received_mail, :closed_mail, :refused_mail, :without_continuation_mail, :re_instructed_mail
+ klass = "Mails::#{error.attribute.to_s.classify}".constantize
+ edit_admin_procedure_mail_template_path(@procedure, klass.const_get(:SLUG))
+ end
+ end
+
+ def to_error_descriptor(error)
+ libelle = case error.attribute
+ when :draft_types_de_champ_public, :draft_types_de_champ_private
+ error.options[:type_de_champ].libelle.truncate(200)
+ else
+ error.base.class.human_attribute_name(error.attribute)
+ end
+ ErrorDescriptor.new(error_correction_page(error), libelle, error.message)
+ end
+end
diff --git a/app/components/procedure/errors_summary/errors_summary.html.haml b/app/components/procedure/errors_summary/errors_summary.html.haml
new file mode 100644
index 000000000..e5042916e
--- /dev/null
+++ b/app/components/procedure/errors_summary/errors_summary.html.haml
@@ -0,0 +1,5 @@
+#errors-summary
+ - if invalid?
+ = render Dsfr::AlertComponent.new(state: :error, title: , extra_class_names: 'fr-mb-2w') do |c|
+ - c.with_body do
+ = render ExpandableErrorList.new(errors:)
diff --git a/app/components/procedure/pending_republish_component.rb b/app/components/procedure/pending_republish_component.rb
new file mode 100644
index 000000000..181eb6f5c
--- /dev/null
+++ b/app/components/procedure/pending_republish_component.rb
@@ -0,0 +1,10 @@
+class Procedure::PendingRepublishComponent < ApplicationComponent
+ def initialize(procedure:, render_if:)
+ @procedure = procedure
+ @render_if = render_if
+ end
+
+ def render?
+ @render_if
+ end
+end
diff --git a/app/components/procedure/pending_republish_component/pending_republish_component.fr.yml b/app/components/procedure/pending_republish_component/pending_republish_component.fr.yml
new file mode 100644
index 000000000..eb941cdba
--- /dev/null
+++ b/app/components/procedure/pending_republish_component/pending_republish_component.fr.yml
@@ -0,0 +1,4 @@
+---
+fr:
+ pending_republish_html: |
+ Ces modifications ne seront appliquées qu'à la prochaine publication. Vous pouvez vérifier puis publier les modifications sur l'écran de gestion de la démarche
\ No newline at end of file
diff --git a/app/components/procedure/pending_republish_component/pending_republish_component.html.haml b/app/components/procedure/pending_republish_component/pending_republish_component.html.haml
new file mode 100644
index 000000000..eab7f62fc
--- /dev/null
+++ b/app/components/procedure/pending_republish_component/pending_republish_component.html.haml
@@ -0,0 +1,3 @@
+= render Dsfr::AlertComponent.new(state: :warning) do |c|
+ - c.with_body do
+ = t('.pending_republish_html', href: admin_procedure_path(@procedure.id))
diff --git a/app/components/procedure/publication_warning_component.rb b/app/components/procedure/publication_warning_component.rb
deleted file mode 100644
index 7af808c3d..000000000
--- a/app/components/procedure/publication_warning_component.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-class Procedure::PublicationWarningComponent < ApplicationComponent
- def initialize(procedure:)
- @procedure = procedure
- end
-
- def title
- return "Des problèmes empêchent la publication des modifications" if @procedure.publiee?
-
- "Des problèmes empêchent la publication de la démarche"
- end
-
- private
-
- def render?
- @procedure.validate(:publication)
- @procedure.errors.delete(:path)
- @procedure.errors.any?
- end
-
- def error_messages
- @procedure.errors
- .to_hash(full_messages: true)
- .map do |attribute, messages|
- [messages, error_correction_page(attribute)]
- end
- end
-
- def error_correction_page(attribute)
- case attribute
- when :draft_revision
- champs_admin_procedure_path(@procedure)
- when :attestation_template
- edit_admin_procedure_attestation_template_path(@procedure)
- when :initiated_mail, :received_mail, :closed_mail, :refused_mail, :without_continuation_mail, :re_instructed_mail
- klass = "Mails::#{attribute.to_s.classify}".constantize
- edit_admin_procedure_mail_template_path(@procedure, klass.const_get(:SLUG))
- end
- end
-end
diff --git a/app/components/procedure/publication_warning_component/publication_warning_component.html.haml b/app/components/procedure/publication_warning_component/publication_warning_component.html.haml
deleted file mode 100644
index 7d8ff43b7..000000000
--- a/app/components/procedure/publication_warning_component/publication_warning_component.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-= render Dsfr::AlertComponent.new(state: :warning, title:) do |c|
- - c.with_body do
- - error_messages.each do |(messages, path)|
- %p.mt-2
- = messages.to_sentence
- - if path.present?
- = "(#{link_to 'corriger', path, class: 'fr-link'})"
diff --git a/app/components/procedure/revision_changes_component.rb b/app/components/procedure/revision_changes_component.rb
index e266f13e2..af786e6bc 100644
--- a/app/components/procedure/revision_changes_component.rb
+++ b/app/components/procedure/revision_changes_component.rb
@@ -1,9 +1,13 @@
class Procedure::RevisionChangesComponent < ApplicationComponent
- def initialize(changes:, previous_revision:)
- @changes = changes
+ def initialize(new_revision:, previous_revision:)
@previous_revision = previous_revision
- @public_move_changes, @private_move_changes = changes.filter { _1.op == :move }.partition { !_1.private? }
- @delete_champ_warning = !total_dossiers.zero? && !@changes.all?(&:can_rebase?)
+ @new_revision = new_revision
+
+ @tdc_changes = previous_revision.compare_types_de_champ(new_revision)
+ @public_move_changes, @private_move_changes = @tdc_changes.filter { _1.op == :move }.partition { !_1.private? }
+ @delete_champ_warning = !total_dossiers.zero? && !@tdc_changes.all?(&:can_rebase?)
+
+ @ineligibilite_rules_changes = previous_revision.compare_ineligibilite_rules(new_revision)
end
private
diff --git a/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml b/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml
index 10009ce1e..3228c76a8 100644
--- a/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml
+++ b/app/components/procedure/revision_changes_component/revision_changes_component.fr.yml
@@ -80,3 +80,10 @@ fr:
update_expression_reguliere_exemple_text: L’exemple d’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouvel exemple est « %{to} ».
remove_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été supprimé.
update_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouveau message est « %{to} ».
+ ineligibilite_rules:
+ add: La condition d’inéligibilité « %{new_condition} » a été ajoutée.
+ remove: La condition d’inéligibilité « %{previous_condition} » a été supprimée
+ update: La conditon d’inéligibilité « %{previous_condition} » a été changée pour « %{new_condition} »
+ enabled: "L’inéligibilité des dossiers a été activée"
+ disabled: "L’inéligibilité des dossiers a été désactivée"
+ message_updated: "Le message d’inéligibilité a été changé pour « %{ineligibilite_message} »"
\ No newline at end of file
diff --git a/app/components/procedure/revision_changes_component/revision_changes_component.html.haml b/app/components/procedure/revision_changes_component/revision_changes_component.html.haml
index ba19a0dd9..ed7f550c8 100644
--- a/app/components/procedure/revision_changes_component/revision_changes_component.html.haml
+++ b/app/components/procedure/revision_changes_component/revision_changes_component.html.haml
@@ -2,7 +2,7 @@
- list.with_empty do
= t('.no_changes')
- - @changes.each do |change|
+ - @tdc_changes.each do |change|
- prefix = change.private? ? 'private' : 'public'
- case change.op
- when :add
@@ -176,3 +176,7 @@
- list.with_item do
.fr-alert.fr-alert--warning.fr-mt-1v
= t(".invalid_routing_rules_alert")
+
+ - @ineligibilite_rules_changes.each do |change|
+ - list.with_item do
+ = t(".ineligibilite_rules.#{change.op}", **change.i18n_params)
diff --git a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml
index 980bd064c..d28769213 100644
--- a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml
+++ b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml
@@ -1,5 +1,5 @@
%li.type-de-champ.flex.column.justify-start.fr-mb-5v{ html_options }
- .type-de-champ-container
+ .type-de-champ-container{ id: dom_id(type_de_champ.stable_self, :editor_error) }
- if @errors.present?
.types-de-champ-errors
= @errors
@@ -10,7 +10,7 @@
.flex.justify-start.width-33
.cell.flex.justify-start.column.flex-grow
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ)
- = form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
+ = form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules? || coordinate.used_by_ineligibilite_rules?
.flex.column.justify-start.flex-grow
.cell
@@ -136,6 +136,10 @@
%span
utilisé pour
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
+ - elsif coordinate.used_by_ineligibilite_rules?
+ %span
+ utilisé pour
+ = link_to('l’eligibilité des dossiers', edit_admin_procedure_ineligibilite_rules_path(revision.procedure_id))
- else
= button_to type_de_champ_path, class: 'fr-btn fr-btn--tertiary-no-outline fr-icon-delete-line', title: "Supprimer le champ", method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
%span.sr-only Supprimer
diff --git a/app/components/types_de_champ_editor/editor_component.rb b/app/components/types_de_champ_editor/editor_component.rb
index 0ea2c76cb..288a2c668 100644
--- a/app/components/types_de_champ_editor/editor_component.rb
+++ b/app/components/types_de_champ_editor/editor_component.rb
@@ -17,4 +17,12 @@ class TypesDeChampEditor::EditorComponent < ApplicationComponent
@revision.revision_types_de_champ_public
end
end
+
+ def validation_context
+ if annotations?
+ :types_de_champ_private_editor
+ else
+ :types_de_champ_public_editor
+ end
+ end
end
diff --git a/app/components/types_de_champ_editor/editor_component/editor_component.html.haml b/app/components/types_de_champ_editor/editor_component/editor_component.html.haml
index 71df85e46..d161d0ba2 100644
--- a/app/components/types_de_champ_editor/editor_component/editor_component.html.haml
+++ b/app/components/types_de_champ_editor/editor_component/editor_component.html.haml
@@ -1,6 +1,6 @@
.fr-pb-12w{ 'data-turbo': 'true', id: dom_id(@revision, :types_de_champ_editor) }
.types-de-champ-editor.editor-root
- = render TypesDeChampEditor::ErrorsSummary.new(revision: @revision)
+ = render Procedure::ErrorsSummary.new(procedure: @revision.procedure, validation_context:)
= render TypesDeChampEditor::BlockComponent.new(block: @revision, coordinates: coordinates)
#empty-coordinates{ hidden: coordinates.present? }
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: @revision, is_annotation: annotations?)
diff --git a/app/components/types_de_champ_editor/errors_summary.rb b/app/components/types_de_champ_editor/errors_summary.rb
deleted file mode 100644
index b5367eaf1..000000000
--- a/app/components/types_de_champ_editor/errors_summary.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-class TypesDeChampEditor::ErrorsSummary < ApplicationComponent
- def initialize(revision:)
- @revision = revision
- end
-
- def invalid?
- @revision.invalid?
- end
-
- def condition_errors?
- @revision.errors.include?(:condition)
- end
-
- def header_section_errors?
- @revision.errors.include?(:header_section)
- end
-
- def expression_reguliere_errors?
- @revision.errors.include?(:expression_reguliere)
- end
-
- private
-
- def errors_for(key)
- @revision.errors.filter { _1.attribute == key }
- end
-
- def error_message_for(key)
- errors_for(key)
- .map { |error| error.options[:type_de_champ] }
- .map { |tdc| tag.li(tdc_anchor(tdc, key)) }
- .then { |lis| tag.ul(lis.reduce(&:+)) }
- end
-
- def tdc_anchor(tdc, key)
- tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, key)), data: { turbo: false })
- end
-end
diff --git a/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml b/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml
deleted file mode 100644
index 08d594931..000000000
--- a/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-fr:
- fix_conditional:
- one: 'La logique conditionnelle du champ suivant est invalide, veuillez la corriger :'
- other: 'La logique conditionnelle des champs suivants sont invalides, veuillez les corriger :'
-
- fix_header_section:
- one: 'Le titre de section suivant est invalide, veuillez le corriger :'
- other: 'Les titres de section suivants sont invalides, veuillez les corriger :'
-
- fix_expressions_regulieres:
- one: "L'expression régulière suivante est invalide, veuillez la corriger :"
- other: 'Les expressions régulières suivantes sont invalides, veuillez les corriger :'
diff --git a/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml b/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml
deleted file mode 100644
index 46f599ce5..000000000
--- a/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-#errors-summary
- - if invalid?
- = render Dsfr::AlertComponent.new(state: :warning, title: "Le formulaire contient des erreurs", extra_class_names: 'fr-mb-2w') do |c|
- - c.with_body do
- - if condition_errors?
- %p= t('.fix_conditional', count: errors_for(:condition).size)
- = error_message_for(:condition)
-
- - if header_section_errors?
- %p= t('.fix_header_section', count: errors_for(:header_section).size)
- = error_message_for(:header_section)
-
- - if expression_reguliere_errors?
- %p= t('.fix_expressions_regulieres', count: errors_for(:expression_reguliere).size)
- = error_message_for(:expression_reguliere)
diff --git a/app/components/types_de_champ_editor/header_section_component.rb b/app/components/types_de_champ_editor/header_section_component.rb
index 80271b8e1..39f372dd8 100644
--- a/app/components/types_de_champ_editor/header_section_component.rb
+++ b/app/components/types_de_champ_editor/header_section_component.rb
@@ -31,7 +31,7 @@ class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent
end
def errors?
- !errors.empty?
+ errors.present?
end
def to_html_list(messages)
diff --git a/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml b/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml
index b40967b26..022c2830b 100644
--- a/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml
+++ b/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml
@@ -1,5 +1,5 @@
%div{ id: dom_id(@tdc.stable_self, :header_section) }
- if errors?
- .errors-summary= to_html_list(errors)
+ .errors-summary= errors
= @form.label :header_section_level, "Niveau du titre", for: dom_id(@tdc, :header_section_level)
= @form.select :header_section_level, header_section_options_for_select, {}, id: dom_id(@tdc, :header_section_level), class: 'fr-select width-33'
diff --git a/app/controllers/administrateurs/ineligibilite_rules_controller.rb b/app/controllers/administrateurs/ineligibilite_rules_controller.rb
new file mode 100644
index 000000000..41d6865f9
--- /dev/null
+++ b/app/controllers/administrateurs/ineligibilite_rules_controller.rb
@@ -0,0 +1,74 @@
+module Administrateurs
+ class IneligibiliteRulesController < AdministrateurController
+ before_action :retrieve_procedure
+
+ def edit
+ end
+
+ def change
+ if draft_revision.update(procedure_revision_params)
+ redirect_to edit_admin_procedure_ineligibilite_rules_path(@procedure)
+ else
+ flash[:alert] = draft_revision.errors.full_messages
+ render :edit
+ end
+ end
+
+ def add_row
+ condition = Logic.add_empty_condition_to(draft_revision.ineligibilite_rules)
+ draft_revision.update!(ineligibilite_rules: condition)
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ def delete_row
+ condition = condition_form.delete_row(row_index).to_condition
+ draft_revision.update!(ineligibilite_rules: condition)
+
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ def update
+ condition = condition_form.to_condition
+ draft_revision.update!(ineligibilite_rules: condition)
+
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ def change_targeted_champ
+ condition = condition_form.change_champ(row_index).to_condition
+ draft_revision.update!(ineligibilite_rules: condition)
+ @ineligibilite_rules_component = build_ineligibilite_rules_component
+ end
+
+ private
+
+ def build_ineligibilite_rules_component
+ Conditions::IneligibiliteRulesComponent.new(draft_revision: draft_revision)
+ end
+
+ def draft_revision
+ @procedure.draft_revision
+ end
+
+ def condition_form
+ ConditionForm.new(ineligibilite_rules_params.merge(source_tdcs: draft_revision.types_de_champ_for(scope: :public)))
+ end
+
+ def ineligibilite_rules_params
+ params
+ .require(:procedure_revision)
+ .require(:condition_form)
+ .permit(:top_operator_name, rows: [:targeted_champ, :operator_name, :value])
+ end
+
+ def row_index
+ params[:row_index].to_i
+ end
+
+ def procedure_revision_params
+ params
+ .require(:procedure_revision)
+ .permit(:ineligibilite_message, :ineligibilite_enabled)
+ end
+ end
+end
diff --git a/app/controllers/api/public/v1/base_controller.rb b/app/controllers/api/public/v1/base_controller.rb
index a60f9caeb..7353c83a3 100644
--- a/app/controllers/api/public/v1/base_controller.rb
+++ b/app/controllers/api/public/v1/base_controller.rb
@@ -1,8 +1,12 @@
-class API::Public::V1::BaseController < APIController
+class API::Public::V1::BaseController < ApplicationController
skip_forgery_protection
before_action :check_content_type_is_json, if: -> { request.post? || request.patch? || request.put? }
+ before_action do
+ Current.browser = 'api'
+ end
+
protected
def render_missing_param(param_name)
diff --git a/app/controllers/email_checker_controller.rb b/app/controllers/email_checker_controller.rb
new file mode 100644
index 000000000..19cd0493b
--- /dev/null
+++ b/app/controllers/email_checker_controller.rb
@@ -0,0 +1,5 @@
+class EmailCheckerController < ApplicationController
+ def show
+ render json: EmailChecker.new.check(email: params[:email])
+ end
+end
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index acdfd1332..1fac35eae 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -231,9 +231,9 @@ module Users
def submit_brouillon
@dossier = dossier_with_champs(pj_template: false)
- @errors = submit_dossier_and_compute_errors
+ submit_dossier_and_compute_errors
- if @errors.blank?
+ if @dossier.errors.blank? && @dossier.can_passer_en_construction?
@dossier.passer_en_construction!
@dossier.process_declarative!
@dossier.process_sva_svr!
@@ -278,9 +278,9 @@ module Users
editing_fork_origin.resolve_pending_correction
end
- @errors = submit_dossier_and_compute_errors
+ submit_dossier_and_compute_errors
- if @errors.blank?
+ if @dossier.errors.blank? && @dossier.can_passer_en_construction?
editing_fork_origin.merge_fork(@dossier)
editing_fork_origin.submit_en_construction!
@@ -288,7 +288,6 @@ module Users
else
respond_to do |format|
format.html do
- @dossier = editing_fork_origin
render :modifier
end
@@ -303,10 +302,10 @@ module Users
def update
@dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier
@dossier = dossier_with_champs(pj_template: false)
- @errors = update_dossier_and_compute_errors
-
- @dossier.index_search_terms_later if @errors.empty?
-
+ @can_passer_en_construction_was = @dossier.can_passer_en_construction?
+ update_dossier_and_compute_errors
+ @dossier.index_search_terms_later if @dossier.errors.empty?
+ @can_passer_en_construction_is = @dossier.can_passer_en_construction?
respond_to do |format|
format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?))
@@ -567,21 +566,14 @@ module Users
def submit_dossier_and_compute_errors
@dossier.validate(:champs_public_value)
-
- errors = @dossier.errors
- @dossier.check_mandatory_and_visible_champs.each do |error_on_champ|
- errors.import(error_on_champ)
- end
+ @dossier.check_mandatory_and_visible_champs
if @dossier.editing_fork_origin&.pending_correction?
@dossier.editing_fork_origin.validate(:champs_public_value)
@dossier.editing_fork_origin.errors.where(:pending_correction).each do |error|
- errors.import(error)
+ @dossier.errors.import(error)
end
-
end
-
- errors
end
def ensure_ownership!
diff --git a/app/graphql/api/v2/stored_query.rb b/app/graphql/api/v2/stored_query.rb
index e2b5c78c0..1c48a5bd1 100644
--- a/app/graphql/api/v2/stored_query.rb
+++ b/app/graphql/api/v2/stored_query.rb
@@ -819,6 +819,19 @@ class API::V2::StoredQuery
}
}
+ mutation dossierSupprimerMessage($input: DossierSupprimerMessageInput!) {
+ dossierSupprimerMessage(input: $input) {
+ message {
+ id
+ createdAt
+ discardedAt
+ }
+ errors {
+ message
+ }
+ }
+ }
+
mutation dossierModifierAnnotationText(
$input: DossierModifierAnnotationTextInput!
) {
diff --git a/app/graphql/mutations/dossier_passer_en_instruction.rb b/app/graphql/mutations/dossier_passer_en_instruction.rb
index 1ece00ede..d9886b724 100644
--- a/app/graphql/mutations/dossier_passer_en_instruction.rb
+++ b/app/graphql/mutations/dossier_passer_en_instruction.rb
@@ -21,6 +21,9 @@ module Mutations
if !dossier.en_construction?
return false, { errors: ["Le dossier est déjà #{dossier_display_state(dossier, lower: true)}"] }
end
+ if dossier.blocked_with_pending_correction?
+ return false, { errors: ["Le dossier est en attente de correction"] }
+ end
dossier_authorized_for?(dossier, instructeur)
end
end
diff --git a/app/graphql/mutations/dossier_supprimer_message.rb b/app/graphql/mutations/dossier_supprimer_message.rb
new file mode 100644
index 000000000..bde6de51f
--- /dev/null
+++ b/app/graphql/mutations/dossier_supprimer_message.rb
@@ -0,0 +1,24 @@
+module Mutations
+ class DossierSupprimerMessage < Mutations::BaseMutation
+ description "Supprimer un message."
+
+ argument :message_id, ID, required: true, loads: Types::MessageType
+ argument :instructeur_id, ID, required: true, loads: Types::ProfileType
+
+ field :message, Types::MessageType, null: true
+ field :errors, [Types::ValidationErrorType], null: true
+
+ def resolve(message:, **args)
+ message.soft_delete!
+
+ { message: }
+ end
+
+ def authorized?(message:, instructeur:, **args)
+ if !message.soft_deletable?(instructeur)
+ return false, { errors: ["Le message ne peut pas être supprimé"] }
+ end
+ dossier_authorized_for?(message.dossier, instructeur)
+ end
+ end
+end
diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql
index 52d58d1ad..c422408fe 100644
--- a/app/graphql/schema.graphql
+++ b/app/graphql/schema.graphql
@@ -2192,6 +2192,30 @@ enum DossierState {
sans_suite
}
+"""
+Autogenerated input type of DossierSupprimerMessage
+"""
+input DossierSupprimerMessageInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+ instructeurId: ID!
+ messageId: ID!
+}
+
+"""
+Autogenerated return type of DossierSupprimerMessage.
+"""
+type DossierSupprimerMessagePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+ errors: [ValidationError!]
+ message: Message
+}
+
type DropDownListChampDescriptor implements ChampDescriptor {
"""
Description des champs d’un bloc répétable.
@@ -3084,6 +3108,7 @@ type Message {
body: String!
correction: Correction
createdAt: ISO8601DateTime!
+ discardedAt: ISO8601DateTime
email: String!
id: ID!
}
@@ -3313,6 +3338,16 @@ type Mutation {
input: DossierRepasserEnInstructionInput!
): DossierRepasserEnInstructionPayload
+ """
+ Supprimer un message.
+ """
+ dossierSupprimerMessage(
+ """
+ Parameters for DossierSupprimerMessage
+ """
+ input: DossierSupprimerMessageInput!
+ ): DossierSupprimerMessagePayload
+
"""
Ajouter des instructeurs à un groupe instructeur.
"""
diff --git a/app/graphql/types/message_type.rb b/app/graphql/types/message_type.rb
index 70622647b..788075bb2 100644
--- a/app/graphql/types/message_type.rb
+++ b/app/graphql/types/message_type.rb
@@ -4,6 +4,7 @@ module Types
field :email, String, null: false
field :body, String, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
+ field :discarded_at, GraphQL::Types::ISO8601DateTime, null: true
field :attachment, Types::File, null: true, deprecation_reason: "Utilisez le champ `attachments` à la place.", extensions: [
{ Extensions::Attachment => { attachments: :piece_jointe, as: :single } }
]
@@ -19,5 +20,9 @@ module Types
def correction
Loaders::Association.for(object.class, :dossier_correction).load(object)
end
+
+ def self.authorized?(object, context)
+ context.authorized_demarche?(object.dossier.revision.procedure)
+ end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ce2f8ac56..b9ad5a92c 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -3,6 +3,7 @@ module Types
field :create_direct_upload, mutation: Mutations::CreateDirectUpload
field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage
+ field :dossier_supprimer_message, mutation: Mutations::DossierSupprimerMessage
field :dossier_passer_en_instruction, mutation: Mutations::DossierPasserEnInstruction
field :dossier_classer_sans_suite, mutation: Mutations::DossierClasserSansSuite
field :dossier_refuser, mutation: Mutations::DossierRefuser
diff --git a/app/helpers/gallery_helper.rb b/app/helpers/gallery_helper.rb
new file mode 100644
index 000000000..1f9ddeeab
--- /dev/null
+++ b/app/helpers/gallery_helper.rb
@@ -0,0 +1,27 @@
+module GalleryHelper
+ def displayable_pdf?(blob)
+ blob.previewable? && blob.content_type.in?(AUTHORIZED_PDF_TYPES)
+ end
+
+ def displayable_image?(blob)
+ blob.variable? && blob.content_type.in?(AUTHORIZED_IMAGE_TYPES)
+ end
+
+ def preview_url_for(attachment)
+ attachment.preview(resize_to_limit: [400, 400]).processed.url
+ rescue StandardError
+ 'pdf-placeholder.png'
+ end
+
+ def variant_url_for(attachment)
+ attachment.variant(resize_to_limit: [400, 400]).processed.url
+ rescue StandardError
+ 'apercu-indisponible.png'
+ end
+
+ def blob_url(attachment)
+ attachment.blob.content_type.in?(RARE_IMAGE_TYPES) ? attachment.variant(resize_to_limit: [2000, 2000]).processed.url : attachment.blob.url
+ rescue StandardError
+ attachment.blob.url
+ end
+end
diff --git a/app/javascript/controllers/clipboard_controller.ts b/app/javascript/controllers/clipboard_controller.ts
index d2b4ae1cf..cecdc5d93 100644
--- a/app/javascript/controllers/clipboard_controller.ts
+++ b/app/javascript/controllers/clipboard_controller.ts
@@ -17,7 +17,11 @@ export class ClipboardController extends Controller {
connect(): void {
// some extensions or browsers block clipboard
if (!navigator.clipboard) {
- this.element.classList.add('hidden');
+ if (this.hasToHideTarget) {
+ this.toHideTarget.classList.add('hidden');
+ } else {
+ this.element.classList.add('hidden');
+ }
}
}
diff --git a/app/javascript/controllers/email_input_controller.ts b/app/javascript/controllers/email_input_controller.ts
index 8eed97fa9..f8442e1d3 100644
--- a/app/javascript/controllers/email_input_controller.ts
+++ b/app/javascript/controllers/email_input_controller.ts
@@ -1,18 +1,43 @@
-import { suggest } from 'email-butler';
+import { httpRequest } from '@utils';
import { show, hide } from '@utils';
import { ApplicationController } from './application_controller';
+type checkEmailResponse = {
+ success: boolean;
+ email_suggestions: string[];
+};
+
export class EmailInputController extends ApplicationController {
static targets = ['ariaRegion', 'suggestion', 'input'];
+ static values = {
+ url: String
+ };
+
+ declare readonly urlValue: string;
+
declare readonly ariaRegionTarget: HTMLElement;
declare readonly suggestionTarget: HTMLElement;
declare readonly inputTarget: HTMLInputElement;
- checkEmail() {
- const suggestion = suggest(this.inputTarget.value);
- if (suggestion && suggestion.full) {
- this.suggestionTarget.innerHTML = suggestion.full;
+ async checkEmail() {
+ if (
+ !this.inputTarget.value ||
+ this.inputTarget.value.length < 5 ||
+ !this.inputTarget.value.includes('@')
+ ) {
+ return;
+ }
+
+ const url = new URL(this.urlValue, document.baseURI);
+ url.searchParams.append('email', this.inputTarget.value);
+
+ const data: checkEmailResponse | null = await httpRequest(
+ url.toString()
+ ).json();
+
+ if (data && data.email_suggestions && data.email_suggestions.length > 0) {
+ this.suggestionTarget.innerHTML = data.email_suggestions[0];
show(this.ariaRegionTarget);
this.ariaRegionTarget.setAttribute('aria-live', 'assertive');
}
diff --git a/app/javascript/controllers/ineligibilite_rules_match_controller.ts b/app/javascript/controllers/ineligibilite_rules_match_controller.ts
new file mode 100644
index 000000000..5b47d79b5
--- /dev/null
+++ b/app/javascript/controllers/ineligibilite_rules_match_controller.ts
@@ -0,0 +1,19 @@
+import { ApplicationController } from './application_controller';
+declare interface modal {
+ disclose: () => void;
+}
+declare interface dsfr {
+ modal: modal;
+}
+declare const window: Window &
+ typeof globalThis & { dsfr: (elem: HTMLElement) => dsfr };
+
+export class InvalidIneligibiliteRulesController extends ApplicationController {
+ static targets = ['dialog'];
+
+ declare dialogTarget: HTMLElement;
+
+ connect() {
+ setTimeout(() => window.dsfr(this.dialogTarget).modal.disclose(), 100);
+ }
+}
diff --git a/app/jobs/image_processor_job.rb b/app/jobs/image_processor_job.rb
index 8017d2b35..ee914a925 100644
--- a/app/jobs/image_processor_job.rb
+++ b/app/jobs/image_processor_job.rb
@@ -10,6 +10,10 @@ class ImageProcessorJob < ApplicationJob
# (to avoid modifying the file while it is being scanned).
retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
+ rescue_from ActiveStorage::PreviewError do
+ retry_or_discard
+ end
+
def perform(blob)
return if blob.nil?
raise FileNotScannedYetError if blob.virus_scanner.pending?
@@ -38,6 +42,9 @@ class ImageProcessorJob < ApplicationJob
blob.attachments.each do |attachment|
next unless attachment&.representable?
attachment.representation(resize_to_limit: [400, 400]).processed
+ if attachment.blob.content_type.in?(RARE_IMAGE_TYPES)
+ attachment.variant(resize_to_limit: [2000, 2000]).processed
+ end
end
end
@@ -55,4 +62,14 @@ class ImageProcessorJob < ApplicationJob
end
end
end
+
+ def retry_or_discard
+ if executions < max_attempts
+ retry_job wait: 5.minutes
+ end
+ end
+
+ def max_attempts
+ 3
+ end
end
diff --git a/app/lib/active_job/application_log_subscriber.rb b/app/lib/active_job/application_log_subscriber.rb
index 23d31f072..a24b293b7 100644
--- a/app/lib/active_job/application_log_subscriber.rb
+++ b/app/lib/active_job/application_log_subscriber.rb
@@ -33,6 +33,7 @@ class ActiveJob::ApplicationLogSubscriber < ::ActiveJob::LogSubscriber
def process_event(event, type)
data = extract_metadata(event)
data.merge!(extract_exception(event))
+ data[:request_id] = Current.request_id if Current.request_id.present?
case type
when 'enqueue_at'
diff --git a/app/lib/balancer_delivery_method.rb b/app/lib/balancer_delivery_method.rb
index 2ed33ae91..4ad5d5728 100644
--- a/app/lib/balancer_delivery_method.rb
+++ b/app/lib/balancer_delivery_method.rb
@@ -45,6 +45,7 @@ class BalancerDeliveryMethod
def prevent_delivery?(mail)
return false if mail[BYPASS_UNVERIFIED_MAIL_PROTECTION].present?
+ return false if mail.to.blank? # bcc list
user = User.find_by(email: mail.to.first)
return user.unverified_email? if user.present?
diff --git a/app/lib/email_checker.rb b/app/lib/email_checker.rb
new file mode 100644
index 000000000..c2cbe3536
--- /dev/null
+++ b/app/lib/email_checker.rb
@@ -0,0 +1,652 @@
+class EmailChecker
+ # Extracted 100 most used domain on our users table [june 2024]
+ # + all .gouv.fr domain on our users table
+ # + all .ac-xxx on our users table
+ KNOWN_DOMAINS = [
+ 'gmail.com',
+ 'hotmail.fr',
+ 'orange.fr',
+ 'yahoo.fr',
+ 'hotmail.com',
+ 'outlook.fr',
+ 'wanadoo.fr',
+ 'free.fr',
+ 'yahoo.com',
+ 'icloud.com',
+ 'laposte.net',
+ 'live.fr',
+ 'sfr.fr',
+ 'outlook.com',
+ 'neuf.fr',
+ 'aol.com',
+ 'bbox.fr',
+ 'msn.com',
+ 'me.com',
+ 'gmx.fr',
+ 'protonmail.com',
+ 'club-internet.fr',
+ 'live.com',
+ 'ymail.com',
+ 'ars.sante.fr',
+ 'mail.ru',
+ 'cegetel.net',
+ 'numericable.fr',
+ 'aliceadsl.fr',
+ 'comcast.net',
+ 'assurance-maladie.fr',
+ 'mac.com',
+ 'naver.com',
+ 'airbus.com',
+ 'justice.fr',
+ 'pole-emploi.fr',
+ 'educagri.fr',
+ 'aphp.fr',
+ 'netcourrier.com',
+ 'dbmail.com',
+ 'aol.fr',
+ 'qq.com',
+ 'hotmail.co.uk',
+ 'yahoo.co.uk',
+ 'proxima-mail.fr',
+ 'yahoo.com.br',
+ 'sciencespo.fr',
+ 'gmx.com',
+ 'etu.univ-st-etienne.fr',
+ 'yahoo.ca',
+ '163.com',
+ 'francetravail.fr',
+ 'mail.pf',
+ 'nantesmetropole.fr',
+ 'hotmail.it',
+ 'sbcglobal.net',
+ 'noos.fr',
+ 'ird.fr',
+ 'safrangroup.com',
+ 'croix-rouge.fr',
+ 'eiffage.com',
+ 'veolia.com',
+ 'notaires.fr',
+ 'nordnet.fr',
+ 'videotron.ca',
+ 'paris.fr',
+ 'lilo.org',
+ 'mfr.asso.fr',
+ 'yopmail.com',
+ 'ukr.net',
+ 'onf.fr',
+ 'stellantis.com',
+ '9online.fr',
+ 'atmp50.fr',
+ 'engie.com',
+ 'libertysurf.fr',
+ 'mailo.com',
+ 'auchan.fr',
+ 'verizon.net',
+ 'rocketmail.com',
+ 'mpsa.com',
+ 'entrepreneur.fr',
+ 'googlemail.com',
+ 'arcelormittal.com',
+ 'groupe-sos.org',
+ 'proton.me',
+ 'att.net',
+ 'pm.me',
+ 'orange.com',
+ 'abv.bg',
+ 'yahoo.es',
+ 'creditmutuel.fr',
+ 'yandex.ru',
+ 'essec.edu',
+ 'urssaf.fr',
+ 'bpifrance.fr',
+ 'uol.com.br',
+ 'suez.com',
+ 'univ-st-etienne.fr',
+ 'korian.fr',
+ 'developpement-durable.gouv.fr',
+ 'modernisation.gouv.fr',
+ 'social.gouv.fr',
+ 'emploi.gouv.fr',
+ 'agriculture.gouv.fr',
+ 'intradef.gouv.fr',
+ 'interieur.gouv.fr',
+ 'oise.gouv.fr',
+ 'direccte.gouv.fr',
+ 'culture.gouv.fr',
+ 'pas-de-calais.gouv.fr',
+ 'finances.gouv.fr',
+ 'drieets.gouv.fr',
+ 'drjscs.gouv.fr',
+ 'sg.social.gouv.fr',
+ 'martinique.pref.gouv.fr',
+ 'beta.gouv.fr',
+ 'dieccte.gouv.fr',
+ 'cotes-darmor.gouv.fr',
+ 'vosges.gouv.fr',
+ 'developppement-durable.gouv.fr',
+ 'mayenne.gouv.fr',
+ 'aviation-civile.gouv.fr',
+ 'data.gouv.fr',
+ 'recherche.gouv.fr',
+ 'sante.gouv.fr',
+ 'paris-idf.gouv.fr',
+ 'guyane.gouv.fr',
+ 'douane.finances.gouv.fr',
+ 'cget.gouv.fr',
+ 'herault.gouv.fr',
+ 'loire-atlantique.gouv.fr',
+ 'manche.gouv.fr',
+ 'seine-maritime.gouv.fr',
+ 'dgccrf.finances.gouv.fr',
+ 'tarn-et-garonne.gouv.fr',
+ 'dila.gouv.fr',
+ 'diplomatie.gouv.fr',
+ 'haut-rhin.gouv.fr',
+ 'nord.gouv.fr',
+ 'bouches-du-rhone.gouv.fr',
+ 'alpes-de-haute-provence.gouv.fr',
+ 'hautes-alpes.gouv.fr',
+ 'alpes-maritimes.gouv.fr',
+ 'var.gouv.fr',
+ 'vaucluse.gouv.fr',
+ 'rhone.gouv.fr',
+ 'occitanie.gouv.fr',
+ 'ille-et-vilaine.gouv.fr',
+ 'finistere.gouv.fr',
+ 'aisne.gouv.fr',
+ 'indre.gouv.fr',
+ 'yvelines.gouv.fr',
+ 'bas-rhin.gouv.fr',
+ 'landes.gouv.fr',
+ 'haute-marne.gouv.fr',
+ 'correze.gouv.fr',
+ 'val-doise.gouv.fr',
+ 'seine-et-marne.gouv.fr',
+ 'essonne.gouv.fr',
+ 'calvados.gouv.fr',
+ 'charente-maritime.gouv.fr',
+ 'corse-du-sud.gouv.fr',
+ 'gironde.gouv.fr',
+ 'haute-corse.gouv.fr',
+ 'morbihan.gouv.fr',
+ 'pyrenees-atlantiques.gouv.fr',
+ 'pyrenees-orientales.gouv.fr',
+ 'somme.gouv.fr',
+ 'vendee.gouv.fr',
+ 'dgtresor.gouv.fr',
+ 'marne.gouv.fr',
+ 'auvergne-rhone-alpes.gouv.fr',
+ 'meurthe-et-moselle.gouv.fr',
+ 'pm.gouv.fr',
+ 'oncfs.gouv.fr',
+ 'orne.gouv.fr',
+ 'charente.gouv.fr',
+ 'travail.gouv.fr',
+ 'gard.gouv.fr',
+ 'maine-et-loire.gouv.fr',
+ 'moselle.gouv.fr',
+ 'outre-mer.gouv.fr',
+ 'jscs.gouv.fr',
+ 'haute-garonne.gouv.fr',
+ 'vienne.gouv.fr',
+ 'dordogne.gouv.fr',
+ 'eure.gouv.fr',
+ 'meuse.gouv.fr',
+ 'savoie.gouv.fr',
+ 'doubs.gouv.fr',
+ 'bfc.gouv.fr',
+ 'education.gouv.fr',
+ 'ariege.gouv.fr',
+ 'normandie.gouv.fr',
+ 'gendarmerie.interieur.gouv.fr',
+ 'ain.gouv.fr',
+ 'ardennes.gouv.fr',
+ 'drome.gouv.fr',
+ 'bretagne.gouv.fr',
+ 'paca.gouv.fr',
+ 'haute-saone.gouv.fr',
+ 'lot.gouv.fr',
+ 'dgfip.finances.gouv.fr',
+ 'aveyron.gouv.fr',
+ 'gers.gouv.fr',
+ 'tarn.gouv.fr',
+ 'aude.gouv.fr',
+ 'lozere.gouv.fr',
+ 'hautes-pyrenees.gouv.fr',
+ 'jeunesse-sports.gouv.fr',
+ 'alpes.maritimes.gouv.fr',
+ 'dreets.gouv.fr',
+ 'justice.gouv.fr',
+ 'sports.gouv.fr',
+ 'nouvelle-aquitaine.gouv.fr',
+ 'jura.gouv.fr',
+ 'haute-savoie.gouv.fr',
+ 'creuse.gouv.fr',
+ 'creps-poitiers.sports.gouv.fr',
+ 'equipement-agriculture.gouv.fr',
+ 'ira-metz.gouv.fr',
+ 'loire.gouv.fr',
+ 'defense.gouv.fr',
+ 'paris.gouv.fr',
+ 'ensm.sports.gouv.fr',
+ 'isere.gouv.fr',
+ 'haute-loire.gouv.fr',
+ 'cantal.gouv.fr',
+ 'lot-et-garonne.gouv.fr',
+ 'reunion.pref.gouv.fr',
+ 'loiret.gouv.fr',
+ 'indre-et-loire.gouv.fr',
+ 'eleve.ira-metz.gouv.fr',
+ 'deux-sevres.gouv.fr',
+ 'inao.gouv.fr',
+ 'franceconnect.gouv.fr',
+ 'essone.gouv.fr',
+ 'workinfrance.beta.gouv.fr',
+ 'seine-saint-denis.gouv.fr',
+ 'val-de-marne.gouv.fr',
+ 'morbihan.pref.gouv.fr',
+ 'externes.justice.gouv.fr',
+ 'haute-vienne.gouv.fr',
+ 'territoire-de-belfort.gouv.fr',
+ 'creps-reunion.sports.gouv.fr',
+ 'creps-centre.sports.gouv.fr',
+ 'creps-rhonealpes.sports.gouv.fr',
+ 'creps-montpellier.sports.gouv.fr',
+ 'nord.pref.gouv.fr',
+ 'charente-maritime.pref.gouv.fr',
+ 'cher.gouv.fr',
+ 'cote-dor.gouv.fr',
+ 'ssi.gouv.fr',
+ 'ira.gouv.fr',
+ 'pays-de-la-loire.gouv.fr',
+ 'loir-et-cher.gouv.fr',
+ 'saone-et-loire.gouv.fr',
+ 'enseignementsup.gouv.fr',
+ 'eure-et-loir.gouv.fr',
+ 'yonne.gouv.fr',
+ 'guadeloupe.pref.gouv.fr',
+ 'centre-val-de-loire.gouv.fr',
+ 'entreprise.api.gouv.fr',
+ 'grand-est.gouv.fr',
+ 'sarthe.gouv.fr',
+ 'sarthe.pref.gouv.fr',
+ 'puy-de-dome.gouv.fr',
+ 'externes.sante.gouv.fr',
+ 'allier.gouv.fr',
+ 'aube.gouv.fr',
+ 'nievre.gouv.fr',
+ 'ardeche.gouv.fr',
+ 'api.gouv.fr',
+ 'hauts-de-seine.gouv.fr',
+ 'hauts-de-france.gouv.fr',
+ 'temp-beta.gouv.fr',
+ 'def.gouv.fr',
+ 'particulier.api.gouv.fr',
+ 'ira-lille.gouv.fr',
+ 'haute-saone.pref.gouv.fr',
+ 'yvelines.pref.gouv.fr',
+ 'sgg.pm.gouv.fr',
+ 'anah.gouv.fr',
+ 'corse.gouv.fr',
+ 'mayenne.pref.gouv.fr',
+ 'cote-dor.pref.gouv.fr',
+ 'guyane.pref.gouv.fr',
+ 'ira-nantes.gouv.fr',
+ 'igas.gouv.fr',
+ 'tarn.pref.gouv.fr',
+ 'martinique.gouv.fr',
+ 'creps-paca.sports.gouv.fr',
+ 'ofb.gouv.fr',
+ 'loir-et-cher.pref.gouv.fr',
+ 'indre-et-loire.pref.gouv.fr',
+ 'polynesie-francaise.pref.gouv.fr',
+ 'scl.finances.gouv.fr',
+ 'numerique.gouv.fr',
+ 'cantal.pref.gouv.fr',
+ 'territoire-de-belfort.pref.gouv.fr',
+ 'creps-wattignies.sports.gouv.fr',
+ 'vienne.pref.gouv.fr',
+ 'ardennes.pref.gouv.fr',
+ 'creps-strasbourg.sports.gouv.fr',
+ 'creps-dijon.sports.gouv.fr',
+ 'ara.gouv.fr',
+ 'sgdsn.gouv.fr',
+ 'pays-de-la-loire.pref.gouv.fr',
+ 'anct.gouv.fr',
+ 'creps-pap.sports.gouv.fr',
+ 'sgae.gouv.fr',
+ 'esnm.sports.gouv.fr',
+ 'nouvelle-caledonie.gouv.fr',
+ 'deets.gouv.fr',
+ 'mayotte.gouv.fr',
+ 'creps-bordeaux.sports.gouv.fr',
+ 'civs.gouv.fr',
+ 'iga.interieur.gouv.fr',
+ 'cab.travail.gouv.fr',
+ 'ira-bastia.gouv.fr',
+ 'ira-lyon.gouv.fr',
+ 'creps-lorraine.sports.gouv.fr',
+ 'dihal.gouv.fr',
+ 'ofpra.gouv.fr',
+ 'mayotte.pref.gouv.fr',
+ 'strategie.gouv.fr',
+ 'territoires.gouv.fr',
+ 'dgcl.gouv.fr',
+ 'doubs.pref.gouv.fr',
+ 'service-civique.gouv.fr',
+ 'maine-et-loire.pref.gouv.fr',
+ 'envsn.sports.gouv.fr',
+ 'wallis-et-futuna.pref.gouv.fr',
+ 'gendarmerie.defense.gouv.fr',
+ 'anlci.gouv.fr',
+ 'cabinets.finances.gouv.fr',
+ 'seine-maritime.pref.gouv.fr',
+ 'promo46.ira-metz.gouv.fr',
+ 'aisne.pref.gouv.fr',
+ 'sportsdenature.gouv.fr',
+ 'loire-atlantique.pref.gouv.fr',
+ 'aude.pref.gouv.fr',
+ 'premier-ministre.gouv.fr',
+ 'igf.finances.gouv.fr',
+ 'eleves.ira-bastia.gouv.fr',
+ 'igesr.gouv.fr',
+ 'alpc.gouv.fr',
+ 'externes.emploi.gouv.fr',
+ 'prestataire.finances.gouv.fr',
+ 'gironde.pref.gouv.fr',
+ 'premar-atlantique.gouv.fr',
+ 'creps-toulouse.sports.gouv.fr',
+ 'guadeloupe.gouv.fr',
+ 'cybermalveillance.gouv.fr',
+ 'dicod.defense.gouv.fr',
+ 'creps-vichy.sports.gouv.fr',
+ 'aft.gouv.fr',
+ 'equipement.gouv.fr',
+ 'academie.defense.gouv.fr',
+ 'aube.pref.gouv.fr',
+ 'seine-et-marne.pref.gouv.fr',
+ 'pyrenees-orientales.pref.gouv.fr',
+ 'haute-garonne.pref.gouv.fr',
+ 'haut-rhin.pref.gouv.fr',
+ 'seine-saint-denis.pref.gouv.fr',
+ 'dcstep.gouv.fr',
+ 'promo47.ira-metz.gouv.fr',
+ 'trackdechets.beta.gouv.fr',
+ 'val-de-marne.pref.gouv.fr',
+ 'fabrique.social.gouv.fr',
+ 'agrasc.gouv.fr',
+ 'indre.pref.gouv.fr',
+ 'tarn-et-garonne.pref.gouv.fr',
+ 'corse.pref.gouv.fr',
+ 'bas-rhin.pref.gouv.fr',
+ 'inclusion.beta.gouv.fr',
+ 'hauts-de-seine.pref.gouv.fr',
+ 'loiret.pref.gouv.fr',
+ 'essonne.pref.gouv.fr',
+ 'territoires-industrie.gouv.fr',
+ 'spm975.gouv.fr',
+ 'saint-barth-saint-martin.gouv.fr',
+ 'judiciaire.interieur.gouv.fr',
+ 'mer.gouv.fr',
+ 'premar-manche.gouv.fr',
+ 'haute-normandie.pref.gouv.fr',
+ 'prestataire.modernisation.gouv.fr',
+ 'covoiturage.beta.gouv.fr',
+ 'promo48.ira-metz.gouv.fr',
+ 'france-services.gouv.fr',
+ 'ddets.gouv.fr',
+ 'afa.gouv.fr',
+ 'externes.social.gouv.fr',
+ 'vosges.pref.gouv.fr',
+ 'reunion.gouv.fr',
+ 'rhone.pref.gouv.fr',
+ 'alpes-maritimes.pref.gouv.fr',
+ 'gard.pref.gouv.fr',
+ 'oise.pref.gouv.fr',
+ 'creps-reims.sports.gouv.fr',
+ 'bouches-du-rhone.pref.gouv.fr',
+ 'esante.gouv.fr',
+ 'rhone-alpes.pref.gouv.fr',
+ 'finistere.pref.gouv.fr',
+ 'ops-bss.defense.gouv.fr',
+ 'orne.pref.gouv.fr',
+ 'transformation.gouv.fr',
+ 'cbcm.social.gouv.fr',
+ 'recosante.beta.gouv.fr',
+ 'pas-de-calais.pref.gouv.fr',
+ 'promo49.ira-metz.gouv.fr',
+ 'paca.pref.gouv.fr',
+ 'meurthe-et-moselle.pref.gouv.fr',
+ 'externes.sg.social.gouv.fr',
+ 'puy-de-dome.pref.gouv.fr',
+ 'academie.def.gouv.fr',
+ 'tarn.gouv.frd81intranet.ddcspp.tarn.gouv.fr',
+ 'agriculture-equipement.gouv.fr',
+ 'creps-idf.sports.gouv.fr',
+ 'eleve.ira-nantes.gouv.fr',
+ 'cohesion-territoires.gouv.fr',
+ 'ariege.pref.gouv.fr',
+ 'pyrenees-atlantiques.pref.gouv.fr',
+ 'hautes-pyrenees.pref.gouv.fr',
+ 'lot-et-garonne.pref.gouv.fr',
+ 'loire.pref.gouv.fr',
+ 'info-routiere.gouv.fr',
+ 'diges.gouv.fr',
+ 'insp.gouv.fr',
+ 'creps-pdl.sports.gouv.fr',
+ 'ddc.social.gouv.fr',
+ 'eleve.insp.gouv.fr',
+ 'val-doise.pref.gouv.fr',
+ 'montsaintmichel.gouv.fr',
+ 'st-cyr.terre-net.defense.gouv.fr',
+ '.finances.gouv.fr',
+ 'logement.gouv.fr',
+ 'cotes-darmor.pref.gouv.fr',
+ 'marne.pref.gouv.fr',
+ 'herault.pref.gouv.fr',
+ 'viennne.gouv.fr',
+ 'landes.pref.gouv.fr',
+ 'moselle.pref.gouv.fr',
+ 'saone-et-loire.pref.gouv.fr',
+ 'bmpm.gouv.fr',
+ 'ecologie-territoires.gouv.fr',
+ 'nievre.pref.gouv.fr',
+ 'hautes-pyrénées.gouv.fr',
+ 'gic.gouv.fr',
+ 'industrie.gouv.fr',
+ 'lot.pref.gouv.fr',
+ 'plan.gouv.fr',
+ 'internet.gouv.fr',
+ 'mesads.beta.gouv.fr',
+ 'gers.pref.gouv.fr',
+ 'dordogne.pref.gouv.fr',
+ 'somme.pref.gouv.fr',
+ 'datasubvention.beta.gouv.fr',
+ 'anc.gouv.fr',
+ 'premar-mediterranee.gouv.fr',
+ 'ille-et-vilaine.pref.gouv.fr',
+ 'eure-et-loir.pref.gouv.fr',
+ 'prestataires.pm.gouv.fr',
+ 'snu.gouv.fr',
+ 'code.gouv.fr',
+ 'alsace.pref.gouv.fr',
+ 'haute-vienne.pref.gouv.fr',
+ 'yonne.pref.gouv.fr',
+ 'bretagne.pref.gouv.fr',
+ 'mastere.insp.gouv.fr',
+ 'cada.pm.gouv.fr',
+ 'creuse.pref.gouv.fr',
+ 'ecologie.gouv.fr',
+ 'midi-pyrenees.pref.gouv.fr',
+ 'promo54.ira-metz.gouv.fr',
+ 'var.pref.gouv.fr',
+ 'alpes-de-haute-provence.pref.gouv.fr',
+ 'mail.numerique.gouv.fr',
+ 'france-identite.gouv.fr',
+ 'transport.data.gouv.fr',
+ 'allier.pref.gouv.fr',
+ 'dilhal.gouv.fr',
+ 'ardeche.pref.gouv.fr',
+ 'haute-corse.pref.gouv.fr',
+ 'intérieur.gouv.fr',
+ 'ddfip.gouv.fr',
+ 'calvados.pref.gouv.fr',
+ 'territoir-de-belfort.gouv.fr',
+ 'nor.gouv.fr',
+ 'creps-occitanie.sports.gouv.fr',
+ 'developpement-durabe.gouv.fr',
+ 'educ.nat.gouv.fr',
+ 'developpement-duable.gouv.fr',
+ 'dgfip.finanes.gouv.fr',
+ 'loire-atlantqieu.gouv.fr',
+ 'promo55.ira-metz.gouv.fr',
+ 'haute-saône.gouv.fr',
+ 'developpement.durable.gouv.fr',
+ 'dreet.gouv.fr',
+ 'miprof.gouv.fr',
+ 'pref.guyane.gouv.fr',
+ 'developpement.gouv.fr',
+ 'gendamrerie.interieur.gouv.fr',
+ 'pyrenees-atlantique.gouv.fr',
+ 'apprentissage.beta.gouv.fr',
+ 'yveliens.gouv.fr',
+ 'justiice.gouv.fr',
+ 'cutlure.gouv.fr',
+ 'aidantsconnect.beta.gouv.fr',
+ 'developpement-durbale.gouv.fr',
+ 'sine-et-marne.gouv.fr',
+ 'sociale.gouv.fr',
+ 'develeoppement-durable.gouv.fr',
+ 'draaf.gouv.fr',
+ 'drets.gouv.fr',
+ 'ancli.gouv.fr',
+ 'finistrere.gouv.fr',
+ 'bourgogne.pref.gouv.fr',
+ 'ac-polynesie.pf',
+ 'ac-lille.fr',
+ 'ac-nantes.fr',
+ 'ac-martinique.fr',
+ 'ac-creteil.fr',
+ 'ac-toulouse.fr',
+ 'ac-amiensfr',
+ 'ac-amiens.fr',
+ 'ac-rennes.fr',
+ 'ac-strasbourg.fr',
+ 'ac-lyon.fr',
+ 'ac-versailles.fr',
+ 'ac-audit.fr',
+ 'ac-rouen.fr',
+ 'ac-reunion.fr',
+ 'ac-poitiers.fr',
+ 'ac-caen.fr',
+ 'ac-montpellier.fr',
+ 'ac-paris.fr',
+ 'ac-besancon.fr',
+ 'ac-nancy-metz.fr',
+ 'ac-aix-marseille.fr',
+ 'ac-grenoble.fr',
+ 'ac-corse.fr',
+ 'ac-nice.fr',
+ 'ac-orleans-tours.fr',
+ 'ac-guadeloupe.fr',
+ 'ac-reims.fr',
+ 'ac-mayotte.fr',
+ 'ac-clermont.fr',
+ 'ac-bordeaux.fr',
+ 'ac-limoges.fr',
+ 'ac-normandie.fr',
+ 'ac-dijon.fr',
+ 'ac-guyane.fr',
+ 'ac-transports.fr',
+ 'ac-arpajonnais.com',
+ 'ac-cned.fr',
+ 'ac-nettoyage.com',
+ 'ac-architectes.fr',
+ 'ac-ajaccio.corsica',
+ 'ac-noumea.nc',
+ 'ac-spm.fr',
+ 'ac-versailes.fr',
+ 'ac-polynesie.fr',
+ 'ac-experts.fr',
+ 'ac-creteil.com',
+ 'ac-smart-relocation.com',
+ 'ac-ec.pro',
+ 'ac-sas.fr',
+ 'ac-derma.de',
+ 'ac-or.com',
+ 'ac-baugeois.fr',
+ 'ac-5.ru',
+ 'ac-arles.fr',
+ 'ac-holding.net',
+ 'ac-mb.fr',
+ 'ac-wf.wf',
+ 'ac-brest-finistere.fr',
+ 'ac-leman.com',
+ 'ac-darboussier.fr',
+ 'ac-si.fr',
+ 'ac-bordeau.fr',
+ 'ac-gatinais.com',
+ 'ac-cheminots.fr',
+ 'ac-seyssinet.com',
+ 'ac-cannes.fr',
+ 'ac-prev.com',
+ 'ac-sologne.fr',
+ 'ac-rennes',
+ 'ac-courbevoie.com',
+ 'ac-ce.fr',
+ 'ac-architecte.fr',
+ 'ac-tions.org',
+ 'ac-pm.fr',
+ 'ac-avocats.com',
+ 'ac-talents-rh.com',
+ 'ac-louis.com',
+ 'ac-internet.fr',
+ 'ac-toulouse.com',
+ 'ac-escial.fr',
+ 'ac-environnement.com',
+ 'ac-academie.fr',
+ 'ac-poiters.fr',
+ 'ac-bordeux.fr',
+ 'ac-verseilles.fr',
+ 'ac-ais-marseille.fr',
+ 'ac-horizon.fr',
+ 'ac-bordeaux.ft',
+ 'ac-toulouses.fr',
+ 'ac-toulous.fr'
+ ].freeze
+
+ def check(email:)
+ return { success: false } if email.blank?
+
+ parsed_email = Mail::Address.new(EmailSanitizableConcern::EmailSanitizer.sanitize(email))
+ return { success: false } if parsed_email.domain.blank?
+
+ return { success: true } if KNOWN_DOMAINS.any? { _1 == parsed_email.domain }
+
+ similar_domains = closest_domains(domain: parsed_email.domain)
+ return { success: true } if similar_domains.empty?
+
+ { success: true, email_suggestions: email_suggestions(parsed_email:, similar_domains:) }
+ end
+
+ private
+
+ def closest_domains(domain:)
+ KNOWN_DOMAINS.filter do |known_domain|
+ close_by_distance_of(domain, known_domain, distance: 1) ||
+ with_same_chars_and_close_by_distance_of(domain, known_domain, distance: 2)
+ end
+ end
+
+ def close_by_distance_of(a, b, distance:)
+ String::Similarity.levenshtein_distance(a, b) == distance
+ end
+
+ def with_same_chars_and_close_by_distance_of(a, b, distance:)
+ close_by_distance_of(a, b, distance: 2) && a.chars.sort == b.chars.sort
+ end
+
+ def email_suggestions(parsed_email:, similar_domains:)
+ similar_domains.map { Mail::Address.new("#{parsed_email.local}@#{_1}").to_s }
+ end
+end
diff --git a/app/models/champ.rb b/app/models/champ.rb
index 6a05baea3..093829236 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -88,10 +88,6 @@ class Champ < ApplicationRecord
parent_id.present?
end
- def stable_id_with_row
- [row_id, stable_id].compact
- end
-
# used for the `required` html attribute
# check visibility to avoid hidden required input
# which prevent the form from being sent.
diff --git a/app/models/concerns/champ_conditional_concern.rb b/app/models/concerns/champ_conditional_concern.rb
index 9e6559be9..63001229d 100644
--- a/app/models/concerns/champ_conditional_concern.rb
+++ b/app/models/concerns/champ_conditional_concern.rb
@@ -21,6 +21,10 @@ module ChampConditionalConcern
end
end
+ def reset_visible # recompute after a dossier update
+ remove_instance_variable :@visible if instance_variable_defined? :@visible
+ end
+
private
def champs_for_condition
diff --git a/app/models/concerns/dossier_rebase_concern.rb b/app/models/concerns/dossier_rebase_concern.rb
index dd6395dc2..49807793c 100644
--- a/app/models/concerns/dossier_rebase_concern.rb
+++ b/app/models/concerns/dossier_rebase_concern.rb
@@ -22,7 +22,7 @@ module DossierRebaseConcern
end
def pending_changes
- procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
+ procedure.published_revision.present? ? revision.compare_types_de_champ(procedure.published_revision) : []
end
def can_rebase_mandatory_change?(stable_id)
diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb
index 373ad3018..ae899dc04 100644
--- a/app/models/concerns/tags_substitution_concern.rb
+++ b/app/models/concerns/tags_substitution_concern.rb
@@ -307,7 +307,8 @@ module TagsSubstitutionConcern
def format_date(date)
if date.present?
- date.strftime('%d/%m/%Y')
+ format = defined?(self.class::FORMAT_DATE) ? self.class::FORMAT_DATE : '%d/%m/%Y'
+ date.strftime(format)
else
''
end
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index dbfce2464..343baf06d 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -59,7 +59,7 @@ class Dossier < ApplicationRecord
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
has_many :followers_instructeurs, through: :follows, source: :instructeur
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
- has_many :avis, inverse_of: :dossier, dependent: :destroy
+ has_many :avis, -> { order(:created_at) }, inverse_of: :dossier, dependent: :destroy
has_many :experts, through: :avis
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do
def passer_en_construction(instructeur: nil, processed_at: Time.zone.now)
@@ -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
@@ -558,8 +558,18 @@ class Dossier < ApplicationRecord
false
end
+ def blocked_with_pending_correction?
+ 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 procedure.feature_enabled?(:blocking_pending_correction) && pending_correction?
+ return false if blocked_with_pending_correction?
true
end
@@ -932,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)
@@ -1006,7 +1017,6 @@ class Dossier < ApplicationRecord
else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end
-
if procedure.chorusable? && procedure.chorus_configuration.complete?
columns += [
['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel&.fetch("code") { '' }],
diff --git a/app/models/export_template.rb b/app/models/export_template.rb
index f5ed164b9..550bb57cd 100644
--- a/app/models/export_template.rb
+++ b/app/models/export_template.rb
@@ -7,6 +7,7 @@ class ExportTemplate < ApplicationRecord
validates_with ExportTemplateValidator
DOSSIER_STATE = Dossier.states.fetch(:en_construction)
+ FORMAT_DATE = "%Y-%m-%d"
def set_default_values
content["default_dossier_directory"] = tiptap_json("dossier-")
@@ -48,7 +49,7 @@ class ExportTemplate < ApplicationRecord
def attachment_and_path(dossier, attachment, index: 0, row_index: nil, champ: nil)
[
attachment,
- path(dossier, attachment, index, row_index, champ)
+ path(dossier, attachment, index:, row_index:, champ:)
]
end
@@ -116,7 +117,7 @@ class ExportTemplate < ApplicationRecord
"#{render_attributes_for(content["pdf_name"], dossier)}.pdf"
end
- def path(dossier, attachment, index, row_index, champ)
+ def path(dossier, attachment, index: 0, row_index: nil, champ: nil)
if attachment.name == 'pdf_export_for_instructeur'
return export_path(dossier)
end
@@ -128,6 +129,8 @@ class ExportTemplate < ApplicationRecord
'messagerie'
when 'Avis'
'avis'
+ when 'Attestation', 'Etablissement'
+ 'pieces_justificatives'
else
# for attachment
return attachment_path(dossier, attachment, index, row_index, champ)
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 337970e89..7995bd49a 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -259,13 +259,19 @@ class Procedure < ApplicationRecord
validates :lien_dpo, url: { no_local: true, allow_blank: true, accept_email: true }
validates :draft_types_de_champ_public,
+ 'types_de_champ/condition': true,
+ 'types_de_champ/expression_reguliere': true,
+ 'types_de_champ/header_section_consistency': true,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
- on: :publication
+ on: [:types_de_champ_public_editor, :publication]
+
validates :draft_types_de_champ_private,
+ 'types_de_champ/condition': true,
+ 'types_de_champ/header_section_consistency': true,
'types_de_champ/no_empty_block': true,
'types_de_champ/no_empty_drop_down': true,
- on: :publication
+ on: [:types_de_champ_private_editor, :publication]
validate :check_juridique, on: [:create, :publication]
@@ -287,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
@@ -425,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
@@ -553,6 +563,7 @@ class Procedure < ApplicationRecord
procedure.closing_notification_brouillon = false
procedure.closing_notification_en_cours = false
procedure.template = false
+ procedure.monavis_embed = nil
if !procedure.valid?
procedure.errors.attribute_names.each do |attribute|
@@ -1014,6 +1025,13 @@ class Procedure < ApplicationRecord
private
+ def validates_associated_draft_revision_with_context
+ return if draft_revision.blank?
+ return if draft_revision.validate(validation_context)
+
+ draft_revision.errors.map { errors.import(_1) }
+ end
+
def validate_auto_archive_on_in_the_future
return if auto_archive_on.nil?
return if auto_archive_on.future?
diff --git a/app/models/procedure_detail.rb b/app/models/procedure_detail.rb
index ebe6c3f6d..2ae20f20f 100644
--- a/app/models/procedure_detail.rb
+++ b/app/models/procedure_detail.rb
@@ -14,7 +14,7 @@ ProcedureDetail = Struct.new(:id, :libelle, :published_at, :aasm_state, :estimat
end
def parsed_latest_zone_labels
- # Replace curly braces with square brackets to make it a valid JSON array
+ return [] if latest_zone_labels.nil? || latest_zone_labels.strip.empty?
JSON.parse(latest_zone_labels.tr('{', '[').tr('}', ']'))
rescue JSON::ParserError
[]
diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb
index c8daa1bec..0a27fec2c 100644
--- a/app/models/procedure_revision.rb
+++ b/app/models/procedure_revision.rb
@@ -1,4 +1,5 @@
class ProcedureRevision < ApplicationRecord
+ include Logic
self.implicit_order_column = :created_at
belongs_to :procedure, -> { with_discarded }, inverse_of: :revisions, optional: false
belongs_to :dossier_submitted_message, inverse_of: :revisions, optional: true, dependent: :destroy
@@ -17,12 +18,19 @@ class ProcedureRevision < ApplicationRecord
scope :ordered, -> { order(:created_at) }
- validate :conditions_are_valid?
- validate :header_sections_are_valid?
- validate :expressions_regulieres_are_valid?
+ 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)
@@ -140,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)
@@ -255,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
@@ -322,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
@@ -446,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)
@@ -453,48 +497,4 @@ class ProcedureRevision < ApplicationRecord
coordinate.update!(type_de_champ: cloned_type_de_champ)
cloned_type_de_champ
end
-
- def conditions_are_valid?
- public_tdcs = types_de_champ_public.to_a
- .flat_map { _1.repetition? ? children_of(_1) : _1 }
-
- public_tdcs
- .map.with_index
- .filter_map { |tdc, i| tdc.condition? ? [tdc, i] : nil }
- .map do |tdc, i|
- [tdc, tdc.condition.errors(public_tdcs.take(i))]
- end
- .filter { |_tdc, errors| errors.present? }
- .each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
- end
-
- def header_sections_are_valid?
- public_tdcs = types_de_champ_public.to_a
-
- root_tdcs_errors = errors_for_header_sections_order(public_tdcs)
- repetition_tdcs_errors = public_tdcs
- .filter_map { _1.repetition? ? children_of(_1) : nil }
- .map { errors_for_header_sections_order(_1) }
-
- repetition_tdcs_errors + root_tdcs_errors
- end
-
- def expressions_regulieres_are_valid?
- types_de_champ_public.to_a
- .flat_map { _1.repetition? ? children_of(_1) : _1 }
- .each do |tdc|
- if tdc.expression_reguliere? && tdc.invalid_regexp?
- errors.add(:expression_reguliere, type_de_champ: tdc)
- end
- end
- end
-
- def errors_for_header_sections_order(tdcs)
- tdcs
- .map.with_index
- .filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
- .map { |tdc, i| [tdc, tdc.check_coherent_header_level(tdcs.take(i))] }
- .filter { |_tdc, errors| errors.present? }
- .each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) }
- end
end
diff --git a/app/models/procedure_revision_change.rb b/app/models/procedure_revision_change.rb
index fc412cc26..7d99f0fd2 100644
--- a/app/models/procedure_revision_change.rb
+++ b/app/models/procedure_revision_change.rb
@@ -1,17 +1,19 @@
class ProcedureRevisionChange
- attr_reader :type_de_champ
- def initialize(type_de_champ)
- @type_de_champ = type_de_champ
+ class TypeDeChange
+ attr_reader :type_de_champ
+ def initialize(type_de_champ)
+ @type_de_champ = type_de_champ
+ end
+
+ def label = @type_de_champ.libelle
+ def stable_id = @type_de_champ.stable_id
+ def private? = @type_de_champ.private?
+ def child? = @type_de_champ.child?
+
+ def to_h = { op:, stable_id:, label:, private: private? }
end
- def label = @type_de_champ.libelle
- def stable_id = @type_de_champ.stable_id
- def private? = @type_de_champ.private?
- def child? = @type_de_champ.child?
-
- def to_h = { op:, stable_id:, label:, private: private? }
-
- class AddChamp < ProcedureRevisionChange
+ class AddChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@@ -23,7 +25,7 @@ class ProcedureRevisionChange
def to_h = super.merge(mandatory: mandatory?)
end
- class RemoveChamp < ProcedureRevisionChange
+ class RemoveChamp < TypeDeChange
def initialize(type_de_champ)
super(type_de_champ)
end
@@ -32,7 +34,7 @@ class ProcedureRevisionChange
def can_rebase?(dossier = nil) = true
end
- class MoveChamp < ProcedureRevisionChange
+ class MoveChamp < TypeDeChange
attr_reader :from, :to
def initialize(type_de_champ, from, to)
@@ -46,7 +48,7 @@ class ProcedureRevisionChange
def to_h = super.merge(from:, to:)
end
- class UpdateChamp < ProcedureRevisionChange
+ class UpdateChamp < TypeDeChange
attr_reader :attribute, :from, :to
def initialize(type_de_champ, attribute, from, to)
@@ -75,4 +77,48 @@ class ProcedureRevisionChange
end
end
end
+
+ class EligibiliteRulesChange
+ attr_reader :previous_revision, :new_revision
+ def initialize(previous_revision, new_revision)
+ @previous_revision = previous_revision
+ @new_revision = new_revision
+ @previous_ineligibilite_rules = @previous_revision.ineligibilite_rules
+ @new_ineligibilite_rules = @new_revision.ineligibilite_rules
+ end
+
+ def i18n_params
+ {
+ previous_condition: @previous_ineligibilite_rules&.to_s(previous_revision.types_de_champ.filter { @previous_ineligibilite_rules.sources.include? _1.stable_id }),
+ new_condition: @new_ineligibilite_rules&.to_s(new_revision.types_de_champ.filter { @new_ineligibilite_rules.sources.include? _1.stable_id })
+ }
+ end
+ end
+
+ class AddEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :add
+ end
+
+ class RemoveEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :remove
+ end
+
+ class UpdateEligibiliteRuleChange < EligibiliteRulesChange
+ def op = :update
+ end
+
+ class EligibiliteEnabledChange < EligibiliteRulesChange
+ def op = :enabled
+ def i18n_params = {}
+ end
+
+ class EligibiliteDisabledChange < EligibiliteRulesChange
+ def op = :disabled
+ def i18n_params = {}
+ end
+
+ class UpdateEligibiliteMessageChange < EligibiliteRulesChange
+ def op = :message_updated
+ def i18n_params = { ineligibilite_message: @new_revision.ineligibilite_message }
+ end
end
diff --git a/app/models/procedure_revision_type_de_champ.rb b/app/models/procedure_revision_type_de_champ.rb
index c4842da20..506e205f7 100644
--- a/app/models/procedure_revision_type_de_champ.rb
+++ b/app/models/procedure_revision_type_de_champ.rb
@@ -75,4 +75,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
def used_by_routing_rules?
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
end
+
+ def used_by_ineligibilite_rules?
+ revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
+ end
end
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index ea2de7468..cf2c321d8 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -505,15 +505,15 @@ class TypeDeChamp < ApplicationRecord
end
def check_coherent_header_level(upper_tdcs)
- errs = []
previous_level = previous_section_level(upper_tdcs)
-
current_level = header_section_level_value.to_i
+
difference = current_level - previous_level
if current_level > previous_level && difference != 1
- errs << I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
+ I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
+ else
+ nil
end
- errs
end
def current_section_level(revision)
@@ -657,6 +657,10 @@ class TypeDeChamp < ApplicationRecord
type_champ.in?(ROUTABLE_TYPES)
end
+ def conditionable?
+ Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(type_champ)
+ end
+
def invalid_regexp?
self.errors.delete(:expression_reguliere)
self.errors.delete(:expression_reguliere_exemple_text)
diff --git a/app/serializers/commentaire_serializer.rb b/app/serializers/commentaire_serializer.rb
index 2941765e2..94f652147 100644
--- a/app/serializers/commentaire_serializer.rb
+++ b/app/serializers/commentaire_serializer.rb
@@ -2,9 +2,18 @@ class CommentaireSerializer < ActiveModel::Serializer
attributes :email,
:body,
:created_at,
- :piece_jointe_attachments
+ :piece_jointe_attachments,
+ :attachment
def created_at
object.created_at&.in_time_zone('UTC')
end
+
+ def attachment
+ piece_jointe = object.piece_jointe_attachments.first
+
+ if piece_jointe&.virus_scanner&.safe?
+ Rails.application.routes.url_helpers.url_for(piece_jointe)
+ end
+ end
end
diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb
index 0fdbc2be9..92d53b088 100644
--- a/app/services/pieces_justificatives_service.rb
+++ b/app/services/pieces_justificatives_service.rb
@@ -169,7 +169,12 @@ class PiecesJustificativesService
.filter { |a| safe_attachment(a) }
.map do |a|
dossier_id = commentaire_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ @export_template.attachment_and_path(dossier, a)
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -190,7 +195,12 @@ class PiecesJustificativesService
.where(record_type: "Etablissement", record_id: etablissement_id_dossier_id.keys)
.map do |a|
dossier_id = etablissement_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ @export_template.attachment_and_path(dossier, a)
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -201,7 +211,12 @@ class PiecesJustificativesService
.filter { |a| safe_attachment(a) }
.map do |a|
dossier_id = a.record_id
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ @export_template.attachment_and_path(dossier, a)
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -217,7 +232,12 @@ class PiecesJustificativesService
.where(record_type: "Attestation", record_id: attestation_id_dossier_id.keys)
.map do |a|
dossier_id = attestation_id_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ @export_template.attachment_and_path(dossier, a)
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
@@ -241,7 +261,12 @@ class PiecesJustificativesService
.filter { |a| safe_attachment(a) }
.map do |a|
dossier_id = avis_ids_dossier_id[a.record_id]
- ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ if @export_template
+ dossier = dossiers.find { _1.id == dossier_id }
+ @export_template.attachment_and_path(dossier, a)
+ else
+ ActiveStorage::DownloadableFile.pj_and_path(dossier_id, a)
+ end
end
end
diff --git a/app/validators/types_de_champ/condition_validator.rb b/app/validators/types_de_champ/condition_validator.rb
new file mode 100644
index 000000000..86e23ff62
--- /dev/null
+++ b/app/validators/types_de_champ/condition_validator.rb
@@ -0,0 +1,34 @@
+class TypesDeChamp::ConditionValidator < ActiveModel::EachValidator
+ # condition are valid when
+ # tdc.condition.left is present in upper tdcs
+ # in case of types_de_champ_private, we should include types_de_champ_publics too
+ def validate_each(procedure, collection, tdcs)
+ return if tdcs.empty?
+
+ tdcs = tdcs_with_children(procedure, tdcs)
+ tdcs.each_with_index do |tdc, tdc_index|
+ next unless tdc.condition?
+
+ upper_tdcs = []
+ if collection == :draft_types_de_champ_private # in case of private tdc validation, we must include public tdcs
+ upper_tdcs += tdcs_with_children(procedure, procedure.draft_types_de_champ_public)
+ end
+ upper_tdcs += tdcs.take(tdc_index) # we take all upper_tdcs of current tdcs
+
+ errors = tdc.condition.errors(upper_tdcs)
+ next if errors.blank?
+
+ procedure.errors.add(
+ collection,
+ procedure.errors.generate_message(collection, :invalid_condition, { value: tdc.libelle }),
+ type_de_champ: tdc
+ )
+ end
+ end
+
+ # find children in repetitions
+ def tdcs_with_children(procedure, tdcs)
+ tdcs.to_a
+ .flat_map { _1.repetition? ? procedure.draft_revision.children_of(_1) : _1 }
+ end
+end
diff --git a/app/validators/types_de_champ/expression_reguliere_validator.rb b/app/validators/types_de_champ/expression_reguliere_validator.rb
new file mode 100644
index 000000000..39262adeb
--- /dev/null
+++ b/app/validators/types_de_champ/expression_reguliere_validator.rb
@@ -0,0 +1,11 @@
+class TypesDeChamp::ExpressionReguliereValidator < ActiveModel::EachValidator
+ def validate_each(procedure, attribute, types_de_champ)
+ types_de_champ.to_a
+ .flat_map { _1.repetition? ? procedure.draft_revision.children_of(_1) : _1 }
+ .each do |tdc|
+ if tdc.expression_reguliere? && tdc.invalid_regexp?
+ procedure.errors.add(:expression_reguliere, type_de_champ: tdc)
+ end
+ end
+ end
+end
diff --git a/app/validators/types_de_champ/header_section_consistency_validator.rb b/app/validators/types_de_champ/header_section_consistency_validator.rb
new file mode 100644
index 000000000..062b27cf0
--- /dev/null
+++ b/app/validators/types_de_champ/header_section_consistency_validator.rb
@@ -0,0 +1,29 @@
+class TypesDeChamp::HeaderSectionConsistencyValidator < ActiveModel::EachValidator
+ def validate_each(procedure, attribute, types_de_champ)
+ public_tdcs = types_de_champ.to_a
+
+ root_tdcs_errors = errors_for_header_sections_order(procedure, attribute, public_tdcs)
+ repetition_tdcs_errors = public_tdcs
+ .filter_map { _1.repetition? ? procedure.draft_revision.children_of(_1) : nil }
+ .map { errors_for_header_sections_order(procedure, attribute, _1) }
+
+ repetition_tdcs_errors + root_tdcs_errors
+ end
+
+ private
+
+ def errors_for_header_sections_order(procedure, attribute, types_de_champ)
+ types_de_champ
+ .map.with_index
+ .filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
+ .map { |tdc, i| [tdc, tdc.check_coherent_header_level(types_de_champ.take(i))] }
+ .filter { |_tdc, errors| errors.present? }
+ .each do |tdc, message|
+ procedure.errors.add(
+ attribute,
+ procedure.errors.generate_message(attribute, :inconsistent_header_section, { value: tdc.libelle, custom_message: message }),
+ type_de_champ: tdc
+ )
+ end
+ end
+end
diff --git a/app/validators/types_de_champ/no_empty_block_validator.rb b/app/validators/types_de_champ/no_empty_block_validator.rb
index e1ea17739..50356d6ab 100644
--- a/app/validators/types_de_champ/no_empty_block_validator.rb
+++ b/app/validators/types_de_champ/no_empty_block_validator.rb
@@ -11,7 +11,8 @@ class TypesDeChamp::NoEmptyBlockValidator < ActiveModel::EachValidator
if procedure.draft_revision.children_of(parent).empty?
procedure.errors.add(
attribute,
- procedure.errors.generate_message(attribute, :empty_repetition, { value: parent.libelle })
+ procedure.errors.generate_message(attribute, :empty_repetition, { value: parent.libelle }),
+ type_de_champ: parent
)
end
end
diff --git a/app/validators/types_de_champ/no_empty_drop_down_validator.rb b/app/validators/types_de_champ/no_empty_drop_down_validator.rb
index bd8fa21dd..0be4e5406 100644
--- a/app/validators/types_de_champ/no_empty_drop_down_validator.rb
+++ b/app/validators/types_de_champ/no_empty_drop_down_validator.rb
@@ -11,7 +11,8 @@ class TypesDeChamp::NoEmptyDropDownValidator < ActiveModel::EachValidator
if drop_down.drop_down_list_enabled_non_empty_options.empty?
procedure.errors.add(
attribute,
- procedure.errors.generate_message(attribute, :empty_drop_down, { value: drop_down.libelle })
+ procedure.errors.generate_message(attribute, :empty_drop_down, { value: drop_down.libelle }),
+ type_de_champ: drop_down
)
end
end
diff --git a/app/views/administrateurs/conditions/_update.turbo_stream.haml b/app/views/administrateurs/conditions/_update.turbo_stream.haml
index 3fba14ec8..028fb8c0e 100644
--- a/app/views/administrateurs/conditions/_update.turbo_stream.haml
+++ b/app/views/administrateurs/conditions/_update.turbo_stream.haml
@@ -4,7 +4,7 @@
['Configuration des champs']],
preview: @procedure.draft_revision.valid? })
-= turbo_stream.replace 'errors-summary', render(TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision))
+= turbo_stream.replace 'errors-summary', render(Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: @tdc.public? ? :types_de_champ_public_editor : :types_de_champ_private_editor))
- rendered = render @condition_component
diff --git a/app/views/administrateurs/experts_procedures/index.html.haml b/app/views/administrateurs/experts_procedures/index.html.haml
index e1d6b6d88..dc8909f82 100644
--- a/app/views/administrateurs/experts_procedures/index.html.haml
+++ b/app/views/administrateurs/experts_procedures/index.html.haml
@@ -7,102 +7,112 @@
%h1.fr-h2
Avis externes
- .groupe-instructeur
- .card
- .card-title= t('.titles.allow_invite_experts')
- %p= t('.descriptions.allow_invite_experts')
+ = render Dsfr::CalloutComponent.new(title: nil) do |c|
+ - c.with_body do
+ Pendant l'instruction d'un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
+ %p
+ = link_to('Comment gérer les avis externes', t('.experts_doc.url'),
+ title: t('.experts_doc.title'),
+ **external_link_attributes)
+
+ %ul.fr-toggle__list
+ %li
= form_for @procedure,
method: :put,
url: allow_expert_review_admin_procedure_path(@procedure),
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :allow_expert_review, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
+ data: { controller: 'autosubmit', turbo: 'true' } do |f|
+
+ = render Dsfr::ToggleComponent.new(form: f,
+ target: :allow_expert_review,
+ title: t('.titles.allow_invite_experts'),
+ hint: t('.descriptions.allow_invite_experts'),
+ disabled: false,
+ extra_class_names: 'fr-toggle--border-bottom')
- if @procedure.allow_expert_review?
- .card
- .card-title= t('.titles.manage_procedure_experts')
- %p= t('.descriptions.manage_procedure_experts')
+ %li
+ = form_for @procedure,
+ method: :put,
+ url: allow_expert_messaging_admin_procedure_path(@procedure),
+ data: { controller: 'autosubmit', turbo: 'true' } do |f|
+
+ = render Dsfr::ToggleComponent.new(form: f,
+ target: :allow_expert_messaging,
+ title: t('.titles.allow_expert_messaging'),
+ hint: t('.descriptions.allow_expert_messaging'),
+ disabled: false,
+ extra_class_names: 'fr-toggle--border-bottom')
+
+ %li
= form_for @procedure,
method: :put,
url: experts_require_administrateur_invitation_admin_procedure_path(@procedure),
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :experts_require_administrateur_invitation, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
+ data: { controller: 'autosubmit', turbo: 'true' } do |f|
- .card
- .card-title= t('.titles.allow_expert_messaging')
- %p= t('.descriptions.allow_expert_messaging')
- = form_for @procedure,
- method: :put,
- url: allow_expert_messaging_admin_procedure_path(@procedure),
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :allow_expert_messaging, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
+ = render Dsfr::ToggleComponent.new(form: f,
+ target: :experts_require_administrateur_invitation,
+ title: t('.titles.manage_procedure_experts'),
+ hint: t('.descriptions.manage_procedure_experts'),
+ disabled: false)
- - if @procedure.experts_require_administrateur_invitation?
- .card
- .card-title Affecter des experts à la démarche
- = form_for :experts_procedure,
- url: admin_procedure_experts_path(@procedure),
- html: { class: 'form' } do |f|
- .instructeur-wrapper
- %p Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
- %p#experts-emails Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
- = hidden_field_tag :emails, nil
- = react_component("ComboMultiple",
- options: [],
- selected: [], disabled: [],
- group: '.instructeur-wrapper',
- name: 'emails',
- label: 'Emails',
- describedby: 'experts-emails',
- acceptNewValues: true)
+ - if @procedure.experts_require_administrateur_invitation?
+ .card
+ = form_for :experts_procedure,
+ url: admin_procedure_experts_path(@procedure),
+ html: { class: 'form' } do |f|
+
+ .instructeur-wrapper
+ %p#experts-emails Entrez les adresses emails des experts que vous souhaitez ajouter à la liste prédéfinie
+ = hidden_field_tag :emails, nil
+ = react_component("ComboMultiple",
+ options: [],
+ selected: [], disabled: [],
+ group: '.instructeur-wrapper',
+ name: 'emails',
+ label: 'Emails',
+ describedby: 'experts-emails',
+ acceptNewValues: true)
+
+ = f.submit 'Ajouter à la liste', class: 'fr-btn'
- = f.submit 'Affecter à la démarche', class: 'button primary send'
- if @experts_procedure.present?
- %table.table.mt-2
- %thead
- %tr
- %th Liste des experts
- %th Nombre d’avis
- - if @procedure.experts_require_administrateur_invitation
- %th Notifier des décisions sur les dossiers
- %tbody
- - @experts_procedure.each do |expert_procedure|
+ .fr-table.fr-table--no-caption.fr-table--layout-fixed.fr-mt-3w
+ %table
+ %thead
%tr
- %td
- = dsfr_icon('fr-icon-user-fill')
- = expert_procedure.expert.email
- %td.text-center
- = expert_procedure.avis.count
+ %th Liste des experts
+ %th Nombre d’avis
- if @procedure.experts_require_administrateur_invitation
+ %th Notifier des décisions sur les dossiers
+ - if @procedure.experts_require_administrateur_invitation
+ %th Action
+ %tbody
+ - @experts_procedure.each do |expert_procedure|
+ %tr
+ %td
+ = dsfr_icon('fr-icon-user-fill')
+ = expert_procedure.expert.email
%td.text-center
- = form_for expert_procedure,
- url: admin_procedure_expert_path(id: expert_procedure),
- method: :put,
- data: { turbo: true },
- html: { class: 'form procedure-form__column--form no-background' } do |f|
- %label.toggle-switch{ data: { controller: 'autosubmit' } }
- = f.check_box :allow_decision_access, class: 'toggle-switch-checkbox'
- %span.toggle-switch-control.round
- %span.toggle-switch-label.on
- %span.toggle-switch-label.off
- - if @procedure.experts_require_administrateur_invitation
- %td.actions= button_to 'retirer',
- admin_procedure_expert_path(id: expert_procedure, procedure: @procedure),
- method: :delete,
- data: { confirm: "Êtes-vous sûr de vouloir révoquer l'expert « #{expert_procedure.expert.email} » de la démarche #{expert_procedure.procedure.libelle} ? Les instructeurs ne pourront plus lui demander d’avis" },
- class: 'button'
+ = expert_procedure.avis.count
+ - if @procedure.experts_require_administrateur_invitation
+ %td.text-center
+ = form_for expert_procedure,
+ url: admin_procedure_expert_path(id: expert_procedure),
+ method: :put,
+ data: { turbo: true },
+ html: { class: 'form procedure-form__column--form no-background' } do |f|
+ %label.toggle-switch{ data: { controller: 'autosubmit' } }
+ = f.check_box :allow_decision_access, class: 'toggle-switch-checkbox'
+ %span.toggle-switch-control.round
+ %span.toggle-switch-label.on
+ %span.toggle-switch-label.off
+ - if @procedure.experts_require_administrateur_invitation
+ %td.actions= button_to 'retirer',
+ admin_procedure_expert_path(id: expert_procedure, procedure: @procedure),
+ method: :delete,
+ data: { confirm: "Êtes-vous sûr de vouloir révoquer l'expert « #{expert_procedure.expert.email} » de la démarche #{expert_procedure.procedure.libelle} ? Les instructeurs ne pourront plus lui demander d’avis" },
+ class: 'fr-btn fr-btn--secondary'
- else
.blank-tab
%h2.empty-text Aucun expert invité pour le moment.
diff --git a/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml
new file mode 100644
index 000000000..a0ace0eca
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/_update.turbo_stream.haml
@@ -0,0 +1,7 @@
+- rendered = render @ineligibilite_rules_component
+
+- if rendered.present?
+ = turbo_stream.replace dom_id(@procedure.draft_revision, :ineligibilite_rules) do
+ - rendered
+- else
+ = turbo_stream.remove dom_id(@procedure.draft_revision, :ineligibilite_rules)
diff --git a/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/add_row.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/change_targeted_champ.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/delete_row.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/destroy.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/ineligibilite_rules/edit.html.haml b/app/views/administrateurs/ineligibilite_rules/edit.html.haml
new file mode 100644
index 000000000..6eb91d12f
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/edit.html.haml
@@ -0,0 +1,28 @@
+= render partial: 'administrateurs/breadcrumbs',
+ locals: { steps: [['Démarches', admin_procedures_path],
+ [@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['Inéligibilité des dossiers']] }
+
+
+.fr-container
+ .fr-grid-row
+ .fr-col-12.fr-col-offset-md-2.fr-col-md-8
+ %h1.fr-h1 Inéligibilité des dossiers
+
+ = render Dsfr::AlertComponent.new(title: nil, size: :sm, state: :info, heading_level: 'h2', extra_class_names: 'fr-my-2w') do |c|
+ - c.with_body do
+ %p
+ Les dossiers répondant à vos conditions d’inéligibilité ne pourront pas être déposés. Plus d’informations sur l’inéligibilité des dossiers dans la
+ = link_to('doc', ELIGIBILITE_URL, title: "Document sur l’inéligibilité des dossiers", **external_link_attributes)
+
+ - if !@procedure.draft_revision.conditionable_types_de_champ.present?
+ %p.fr-mt-2w.fr-mb-2w
+ Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire :
+ %ul
+ - Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.each do
+ %li= "« #{t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »"
+ %p.fr-mt-2w
+ = link_to 'Ajouter un champ supportant les conditions d’inéligibilité', champs_admin_procedure_path(@procedure), class: 'fr-link fr-icon-arrow-right-line fr-link--icon-right'
+ = render Procedure::FixedFooterComponent.new(procedure: @procedure)
+ - else
+ = render Conditions::IneligibiliteRulesComponent.new(draft_revision: @procedure.draft_revision)
diff --git a/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml b/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml
new file mode 100644
index 000000000..8f9900e50
--- /dev/null
+++ b/app/views/administrateurs/ineligibilite_rules/update.turbo_stream.haml
@@ -0,0 +1 @@
+= render partial: 'update'
diff --git a/app/views/administrateurs/procedures/_detail.html.haml b/app/views/administrateurs/procedures/_detail.html.haml
index 798bb15a6..99b8b1410 100644
--- a/app/views/administrateurs/procedures/_detail.html.haml
+++ b/app/views/administrateurs/procedures/_detail.html.haml
@@ -31,7 +31,7 @@
- if show_detail
%tr.procedure{ id: "procedure_detail_#{procedure.id}" }
- %td.fr-highlight--beige-gris-galet{ colspan: '8' }
+ %td.fr-highlight--green-emeraude{ colspan: '8' }
.fr-container
.fr-col-6
- procedure.administrateurs.uniq.each do |admin|
diff --git a/app/views/administrateurs/procedures/_publication_form.html.haml b/app/views/administrateurs/procedures/_publication_form.html.haml
index 85a8f083c..d8d96870e 100644
--- a/app/views/administrateurs/procedures/_publication_form.html.haml
+++ b/app/views/administrateurs/procedures/_publication_form.html.haml
@@ -2,13 +2,13 @@
url: admin_procedure_publish_path(procedure_id: procedure.id),
method: :put,
html: { class: 'form' } do |f|
- = render Procedure::PublicationWarningComponent.new(procedure: procedure)
+ = render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
.mt-2
- if procedure.draft_changed?
%p.mb-2= t('.draft_changed_procedure_alert')
= render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: 'fr-mb-2w') do |c|
- c.with_body do
- = render Procedure::RevisionChangesComponent.new changes: procedure.revision_changes, previous_revision: procedure.published_revision
+ = render Procedure::RevisionChangesComponent.new new_revision: procedure.draft_revision, previous_revision: procedure.published_revision
- if procedure.close?
= render partial: 'publication_form_inputs', locals: { procedure: procedure, closed_procedures: @closed_procedures, form: f }
- elsif @procedure.brouillon? && @procedure.missing_steps.empty?
diff --git a/app/views/administrateurs/procedures/champs.html.haml b/app/views/administrateurs/procedures/champs.html.haml
index 95ddf2860..9f29496ca 100644
--- a/app/views/administrateurs/procedures/champs.html.haml
+++ b/app/views/administrateurs/procedures/champs.html.haml
@@ -4,12 +4,16 @@
['Champs du formulaire']], preview: @procedure.draft_revision.valid? }
.fr-container
- %h1.fr-h2 Champs du formulaire
+ .flex.justify-between.align-center.fr-mb-3w
+ %h1.fr-h2 Champs du formulaire
+ - if @procedure.revised?
+ = link_to "Voir l'historique des modifications du formulaire", modifications_admin_procedure_path(@procedure), class: 'fr-link'
+
= render NestedForms::FormOwnerComponent.new
.fr-grid-row
= render partial: 'champs_summary'
.fr-col
- = render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision)
+ = render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision, is_annotation: false)
.padded-fixed-footer
.fixed-footer
diff --git a/app/views/administrateurs/procedures/modifications.html.haml b/app/views/administrateurs/procedures/modifications.html.haml
index ea9e9c32b..73b8673bd 100644
--- a/app/views/administrateurs/procedures/modifications.html.haml
+++ b/app/views/administrateurs/procedures/modifications.html.haml
@@ -1,8 +1,11 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_back_path(@procedure)],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
+ ['Champs du formulaire', champs_admin_procedure_path(@procedure)],
['Historique des modifications du formulaire']] }
.fr-container
+ .fr-mb-3w
+ = link_to "Champs du formulaire", champs_admin_procedure_path(@procedure), class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
%h1.fr-h2
Historique des modifications du formulaire
@@ -10,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
@@ -28,7 +30,7 @@
%p= t('.dossiers_en_construction', count: dossiers_en_construction_count)
- elsif !dossiers_en_instruction_count.zero?
%p= t('.dossiers_en_instruction', count: dossiers_en_instruction_count)
- = render Procedure::RevisionChangesComponent.new changes:, previous_revision:
+ = render Procedure::RevisionChangesComponent.new new_revision: revision, previous_revision:
- previous_revision = revision
= render Procedure::FixedFooterComponent.new(procedure: @procedure)
diff --git a/app/views/administrateurs/procedures/new_from_existing.html.haml b/app/views/administrateurs/procedures/new_from_existing.html.haml
index 76ccd4178..228004b1d 100644
--- a/app/views/administrateurs/procedures/new_from_existing.html.haml
+++ b/app/views/administrateurs/procedures/new_from_existing.html.haml
@@ -1,35 +1,34 @@
.container
- if current_administrateur.procedures.brouillons.count == 0
- .card.feedback
- .card-title
- Bienvenue,
- %br
- vous allez pouvoir créer une première démarche de test.
- Celle-ci sera visible uniquement par vous et ne sera publiée nulle part, alors pas de crainte à avoir.
- %br
- %br
- Besoin d’aide ?
- %br
- > Vous pouvez
- = link_to "visionner cette vidéo",
- "https://vimeo.com/261478872",
- target: "_blank"
- %br
- > Vous pouvez lire notre
- = link_to "documentation en ligne",
- ADMINISTRATEUR_TUTORIAL_URL,
- target: "_blank"
-
- %br
- > Vous pouvez enfin
- = link_to "prendre un rendez-vous téléphonique avec nous",
- CALENDLY_URL,
- target: "_blank"
-
- :javascript
- document.addEventListener("DOMContentLoaded", function() {
- $crisp.push(["do", "trigger:run", ["admin-signup"]]);
- });
+ = render Dsfr::CalloutComponent.new(title: nil, icon: "fr-icon-information-line", extra_class_names: 'fr-my-4w') do |c|
+ - c.with_html_body do
+ %p
+ Bienvenue,
+ %br
+ vous allez pouvoir créer une première démarche de test.
+ Celle-ci sera visible uniquement par vous et ne sera publiée nulle part, alors pas de crainte à avoir.
+ %br
+ %br
+ Besoin d’aide ?
+ %br
+ > Vous pouvez
+ = link_to "visionner cette vidéo",
+ "https://vimeo.com/261478872",
+ target: "_blank"
+ %br
+ > Vous pouvez lire notre
+ = link_to "documentation en ligne",
+ ADMINISTRATEUR_TUTORIAL_URL,
+ target: "_blank"
+ %br
+ > Vous pouvez enfin
+ = link_to "prendre un rendez-vous téléphonique avec nous",
+ CALENDLY_URL,
+ target: "_blank"
+ :javascript
+ document.addEventListener("DOMContentLoaded", function() {
+ $crisp.push(["do", "trigger:run", ["admin-signup"]]);
+ });
.form
diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml
index a4c1f16d3..4463a86cf 100644
--- a/app/views/administrateurs/procedures/show.html.haml
+++ b/app/views/administrateurs/procedures/show.html.haml
@@ -5,7 +5,7 @@
.fr-container.procedure-admin-container
%ul.fr-btns-group.fr-btns-group--inline-sm.fr-btns-group--icon-left
- - if @procedure.draft_revision.valid?
+ - if @procedure.validate(:publication)
- if !@procedure.brouillon?
= link_to 'Télécharger', admin_procedure_archives_path(@procedure), class: 'fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-download-line', id: "archive-procedure"
@@ -27,15 +27,11 @@
= link_to 'Clore', admin_procedure_close_path(procedure_id: @procedure.id), class: 'fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-calendar-close-fill', id: "close-procedure-link"
.fr-container
- = render TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision)
-
-- if @procedure.draft_changed?
- .fr-container
+ - 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::PublicationWarningComponent.new(procedure: @procedure)
+ = 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
@@ -44,6 +40,9 @@
- else
%li= button_to 'Publier les modifications', admin_procedure_publication_path(@procedure), class: 'fr-btn', id: 'publish-procedure-link', data: { disable_with: "Publication..." }, disabled: !@procedure.draft_revision.valid? || @procedure.errors.present?, method: :get
%li= button_to "Réinitialiser les modifications", admin_procedure_reset_draft_path(@procedure), class: 'fr-btn fr-btn--secondary fr-mr-2w', data: { confirm: 'Êtes-vous sûr de vouloir réinitialiser les modifications ?' }, method: :put
+ - else
+ = render Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: :publication)
+
- if !@procedure.procedure_expires_when_termine_enabled?
= render partial: 'administrateurs/procedures/suggest_expires_when_termine', locals: { procedure: @procedure }
@@ -72,10 +71,10 @@
= 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)
- = render Procedure::Card::ModificationsComponent.new(procedure: @procedure)
%h3.fr-h6 Pour aller plus loin
.fr-grid-row.fr-grid-row--gutters.fr-mb-5w
diff --git a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml
index 9421b594a..ad121dedd 100644
--- a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml
+++ b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml
@@ -10,9 +10,9 @@
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Configuration des champs']],
- preview: @procedure.draft_revision.valid? })
+ preview: @procedure.validate(@coordinate&.private? ? :types_de_champ_private_editor : :types_de_champ_public_editor) })
-= turbo_stream.replace 'errors-summary', render(TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision))
+= turbo_stream.replace 'errors-summary', render(Procedure::ErrorsSummary.new(procedure: @procedure, validation_context: @coordinate&.private? ? :types_de_champ_private_editor : :types_de_champ_public_editor))
= turbo_stream.replace 'summary', render(partial: 'administrateurs/procedures/champs_summary')
diff --git a/app/views/instructeurs/dossiers/pieces_jointes.html.haml b/app/views/instructeurs/dossiers/pieces_jointes.html.haml
index 58a867ccd..d770433d4 100644
--- a/app/views/instructeurs/dossiers/pieces_jointes.html.haml
+++ b/app/views/instructeurs/dossiers/pieces_jointes.html.haml
@@ -8,20 +8,20 @@
- champ.piece_justificative_file.with_all_variant_records.each do |attachment|
.gallery-item
- blob = attachment.blob
- - if blob.content_type.in?(AUTHORIZED_PDF_TYPES)
+ - if displayable_pdf?(blob)
= link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: "#{champ.libelle} -- #{blob.filename}" do
.thumbnail
- = image_tag("pdf-placeholder.png")
+ = image_tag(preview_url_for(attachment), loading: :lazy)
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
Visualiser
.champ-libelle
= champ.libelle.truncate(25)
= render Attachment::ShowComponent.new(attachment: attachment, truncate: true)
- - elsif blob.content_type.in?(AUTHORIZED_IMAGE_TYPES)
- = link_to image_url(blob.url), title: "#{champ.libelle} -- #{blob.filename}", data: { src: blob.url }, class: 'gallery-link' do
+ - elsif displayable_image?(blob)
+ = link_to image_url(blob_url(attachment)), title: "#{champ.libelle} -- #{blob.filename}", data: { src: blob.url }, class: 'gallery-link' do
.thumbnail
- = image_tag(attachment.variant(resize_to_limit: [400, 400]).processed.url, loading: :lazy)
+ = image_tag(variant_url_for(attachment), loading: :lazy)
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
Visualiser
.champ-libelle
diff --git a/app/views/instructeurs/procedures/exports.html.haml b/app/views/instructeurs/procedures/exports.html.haml
index 0986a977a..793a7d960 100644
--- a/app/views/instructeurs/procedures/exports.html.haml
+++ b/app/views/instructeurs/procedures/exports.html.haml
@@ -23,7 +23,7 @@
- else
= t('.no_export_html', expiration_time: Export::MAX_DUREE_CONSERVATION_EXPORT.in_hours.to_i )
- - if feature_enabled?(:export_template)
+ - if @procedure.feature_enabled?(:export_template)
%h2.fr-mb-1w.fr-mt-8w
Liste des modèles d'export
%p.fr-hint-text
diff --git a/app/views/shared/champs/piece_justificative/_show.html.haml b/app/views/shared/champs/piece_justificative/_show.html.haml
index 734aeadd8..abb301872 100644
--- a/app/views/shared/champs/piece_justificative/_show.html.haml
+++ b/app/views/shared/champs/piece_justificative/_show.html.haml
@@ -8,17 +8,17 @@
- champ.piece_justificative_file.attachments.with_all_variant_records.each do |attachment|
.gallery-item
- blob = attachment.blob
- - if blob.content_type.in?(AUTHORIZED_PDF_TYPES)
+ - if displayable_pdf?(blob)
= link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: "#{champ.libelle} -- #{blob.filename}" do
.thumbnail
- = image_tag("pdf-placeholder.png")
+ = image_tag(preview_url_for(attachment), loading: :lazy)
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
= 'Visualiser'
- - elsif blob.content_type.in?(AUTHORIZED_IMAGE_TYPES)
- = link_to image_url(blob.url), title: "#{champ.libelle} -- #{blob.filename}", data: { src: blob.url }, class: 'gallery-link' do
+ - elsif displayable_image?(blob)
+ = link_to image_url(blob_url(attachment)), title: "#{champ.libelle} -- #{blob.filename}", data: { src: blob.url }, class: 'gallery-link' do
.thumbnail
- = image_tag(attachment.variant(resize_to_limit: [400, 400]).processed.url, loading: :lazy)
+ = image_tag(variant_url_for(attachment), loading: :lazy)
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
= 'Visualiser'
- else
diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml
index d5fff3262..c797d35f2 100644
--- a/app/views/shared/dossiers/_edit.html.haml
+++ b/app/views/shared/dossiers/_edit.html.haml
@@ -10,7 +10,7 @@
= render NestedForms::FormOwnerComponent.new
= form_for dossier_for_editing, url: brouillon_dossier_url(dossier), method: :patch, html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } do |f|
- = render Dossiers::ErrorsFullMessagesComponent.new(dossier: @dossier, errors: @errors || [])
+ = render Dossiers::ErrorsFullMessagesComponent.new(dossier: dossier)
%header.mb-6
.fr-highlight
%p.fr-text--sm
@@ -25,4 +25,6 @@
= render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier)
+ = render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier)
+
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)
diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml
index fb97ab53b..99fee2678 100644
--- a/app/views/support/index.html.haml
+++ b/app/views/support/index.html.haml
@@ -3,8 +3,8 @@
= render partial: "root/footer"
#contact-form
- .container
- %h1.new-h1
+ .fr-container
+ %h1
= t('.contact')
= form_tag contact_path, method: :post, multipart: true, class: 'fr-form-group', data: {controller: :support } do
diff --git a/app/views/users/dossiers/_merci.html.haml b/app/views/users/dossiers/_merci.html.haml
index 3334d0f46..43fe1b97c 100644
--- a/app/views/users/dossiers/_merci.html.haml
+++ b/app/views/users/dossiers/_merci.html.haml
@@ -1,26 +1,30 @@
.merci.text-center.mb-7
- .container
- = image_tag('user/envoi-dossier.svg', alt: '', class: 'mt-8')
- %h1.mt-4.mb-3.mx-0= t('views.users.dossiers.merci.thanks')
- %h2.send.m-2.text-lg
- = t('views.users.dossiers.merci.dossier_send_l1')
- %strong= procedure.libelle
- = t('views.users.dossiers.merci.dossier_send_l2')
- %p.m-2
- = t('views.users.dossiers.merci.dossier_acces_l1')
- %strong= t('views.users.dossiers.merci.dossier_acces_l2')
- %p.m-2
- = t('views.users.dossiers.merci.dossier_edit_l1')
- - if !dossier&.read_only? && !procedure.declarative_accepte? && !procedure.sva_svr_enabled?
- %strong= t('views.users.dossiers.merci.dossier_edit_l2')
- = t('views.users.dossiers.merci.dossier_edit_l3')
- %strong= t('views.users.dossiers.merci.dossier_edit_l4')
- - if procedure.active_dossier_submitted_message
- %p.m-2= procedure.active_dossier_submitted_message.message_on_submit_by_usager
+ .fr-container
+ .fr-grid-row.fr-col-offset-md-2.fr-col-md-8
+ .fr-col-12
+ = image_tag('user/envoi-dossier.svg', alt: '', class: 'mt-8')
+ %h1.fr-mt-4w.fr-mb-3w.mx-0= t('views.users.dossiers.merci.thanks')
+ %h2.send.fr-m-2w.text-lg
+ = t('views.users.dossiers.merci.dossier_send_l1')
+ %strong= procedure.libelle
+ = t('views.users.dossiers.merci.dossier_send_l2')
+ %p.fr-m-2w
+ = t('views.users.dossiers.merci.dossier_acces_l1')
+ %strong= t('views.users.dossiers.merci.dossier_acces_l2')
+ %p.fr-m-2w
+ = t('views.users.dossiers.merci.dossier_edit_l1')
+ - if !dossier&.read_only? && !procedure.declarative_accepte? && !procedure.sva_svr_enabled?
+ %strong= t('views.users.dossiers.merci.dossier_edit_l2')
+ = t('views.users.dossiers.merci.dossier_edit_l3')
+ %strong= t('views.users.dossiers.merci.dossier_edit_l4')
+ - if procedure.active_dossier_submitted_message
+ %p.fr-m-2= procedure.active_dossier_submitted_message.message_on_submit_by_usager
+ %p.justify-center.flex.fr-mb-5w.fr-mt-2w
+ = link_to "#{t('views.users.dossiers.merci.download_dossier')} (PDF)", dossier_path(dossier, format: :pdf), download: "Mon dossier", target: "_blank", rel: "noopener", title: t('views.users.dossiers.show.header.print_dossier'), class: 'fr-btn fr-btn--secondary fr-mx-2w fr-btn--icon-left fr-icon-download-line'
+ = link_to t('views.users.dossiers.merci.acces_dossier'), dossier ? dossier_path(dossier) : "#dossier" , class: 'fr-btn fr-mx-2w'
- .flex.column.align-center
- = link_to t('views.users.dossiers.merci.acces_dossier'), dossier ? dossier_path(dossier) : "#dossier" , class: 'fr-btn fr-btn--xl fr-mt-5w'
- = link_to t('views.users.dossiers.merci.submit_dossier'), commencer_url(procedure.path), class: 'fr-btn fr-btn--secondary fr-mt-3w fr-mb-2w'
+ %hr.fr-hr
+ = link_to t('views.users.dossiers.merci.submit_dossier'), commencer_url(procedure.path), class: 'fr-btn fr-btn--secondary fr-mt-2w'
- if procedure.monavis_embed
.monavis
diff --git a/app/views/users/dossiers/update.turbo_stream.haml b/app/views/users/dossiers/update.turbo_stream.haml
index 91a898ab0..8224c1abd 100644
--- a/app/views/users/dossiers/update.turbo_stream.haml
+++ b/app/views/users/dossiers/update.turbo_stream.haml
@@ -1 +1,7 @@
= render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier }
+
+- if !params.key?(:validate)
+ - if @can_passer_en_construction_was && !@can_passer_en_construction_is
+ = turbo_stream.append('contenu', render(Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: @dossier)))
+ - else @ineligibilite_rules_is_computable
+ = turbo_stream.remove(dom_id(@dossier, :ineligibilite_rules_broken))
diff --git a/bun.lockb b/bun.lockb
index 9cc8b3bb3..7c777a111 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/config/env.example.optional b/config/env.example.optional
index b9cc395e9..79710f04e 100644
--- a/config/env.example.optional
+++ b/config/env.example.optional
@@ -61,6 +61,9 @@ DS_ENV="staging"
# Instance customization: URL of the Routage documentation
# ROUTAGE_URL=""
#
+# Instance customization: URL of the EligibiliteDossier documentation
+# ELIGIBILITE_URL=""
+#
# Instance customization: URL of the accessibility statement
# ACCESSIBILITE_URL=""
diff --git a/config/initializers/02_urls.rb b/config/initializers/02_urls.rb
index d6e031dae..c29ea1d5d 100644
--- a/config/initializers/02_urls.rb
+++ b/config/initializers/02_urls.rb
@@ -37,6 +37,7 @@ CGU_URL = ENV.fetch("CGU_URL", [DOC_URL, "cgu"].join("/"))
MENTIONS_LEGALES_URL = ENV.fetch("MENTIONS_LEGALES_URL", "/mentions-legales")
ACCESSIBILITE_URL = ENV.fetch("ACCESSIBILITE_URL", "/declaration-accessibilite")
ROUTAGE_URL = ENV.fetch("ROUTAGE_URL", [DOC_URL, "/pour-aller-plus-loin/routage"].join("/"))
+ELIGIBILITE_URL = ENV.fetch("ELIGIBILITE_URL", [DOC_URL, "/pour-aller-plus-loin/eligibilite-des-dossiers"].join("/"))
API_DOC_URL = [DOC_URL, "api-graphql"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
WEBHOOK_ALTERNATIVE_DOC_URL = [DOC_URL, "api-graphql", "cas-dusages-exemple-dimplementation", "synchroniser-les-dossiers-modifies-sur-ma-demarche"].join("/")
diff --git a/config/initializers/authorized_content_types.rb b/config/initializers/authorized_content_types.rb
index e5af1c74f..eaa16fbd9 100644
--- a/config/initializers/authorized_content_types.rb
+++ b/config/initializers/authorized_content_types.rb
@@ -15,6 +15,10 @@ AUTHORIZED_IMAGE_TYPES = [
'image/vnd.dwg' # multimedia x 137 auto desk
]
+RARE_IMAGE_TYPES = [
+ 'image/tiff' # multimedia x 3985
+]
+
AUTHORIZED_CONTENT_TYPES = AUTHORIZED_IMAGE_TYPES + AUTHORIZED_PDF_TYPES + [
# multimedia
'video/mp4', # multimedia x 2075
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 5787289e6..69aef694b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -444,6 +444,7 @@ en:
dossier_edit_l2: edit it
dossier_edit_l3: and
dossier_edit_l4: talk with an instructor.
+ download_dossier: Download your file
acces_dossier: Access your file
submit_dossier: Submit an other file
jdma_l1: Help us improve this service!
@@ -607,6 +608,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
@@ -668,6 +670,10 @@ en:
path:
taken: is already used for procedure. You cannot use it because it belongs to another administrator.
invalid: is not valid. It must countain between 3 and 200 characters among a-z, 0-9, '_' and '-'.
+ procedure_revision:
+ attributes:
+ ineligibilite_rules:
+ invalid: are invalid
"dossier/champs":
format: "%{message}"
attributes:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 52100dc78..bc6f19514 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -447,6 +447,7 @@ fr:
dossier_edit_l2: le modifier
dossier_edit_l3: et
dossier_edit_l4: échanger avec un instructeur.
+ download_dossier: Télécharger mon dossier
acces_dossier: Accéder à votre dossier
submit_dossier: Déposer un autre dossier
jdma_l1: Aidez-nous à améliorer ce service !
@@ -611,6 +612,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
@@ -670,6 +672,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:
@@ -741,8 +747,6 @@ fr:
evil_regexp: L'expression régulière que vous avez entrée est potentiellement dangereuse et pourrait entraîner des problèmes de performance
mismatch_regexp: L'exemple doit correspondre à l'expression régulière fournie
syntax_error_regexp: La syntaxe de l'expression régulière n'est pas valide
- empty_repetition: '« %{value} » doit comporter au moins un champ répétable'
- empty_drop_down: '« %{value} » doit comporter au moins un choix sélectionnable'
# procedure_not_draft: "Cette démarche n’est maintenant plus en brouillon."
cadastres_empty:
one: "Aucune parcelle cadastrale sur la zone sélectionnée"
diff --git a/config/locales/models/procedure/en.yml b/config/locales/models/procedure/en.yml
index 2f1d1c8b8..34fc89e35 100644
--- a/config/locales/models/procedure/en.yml
+++ b/config/locales/models/procedure/en.yml
@@ -72,8 +72,30 @@ en:
invalid: 'invalid format'
draft_types_de_champ_public:
format: 'Public field %{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: "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:
+ format: "%{attribute} %{message}"
+ received_mail:
+ format: "%{attribute} %{message}"
+ closed_mail:
+ format: "%{attribute} %{message}"
+ refused_mail:
+ format: "%{attribute} %{message}"
+ without_continuation_mail:
+ format: "%{attribute} %{message}"
+ re_instructed_mail:
+ format: "%{attribute} %{message}"
lien_dpo:
invalid_uri_or_email: "Fill in with an email or a link"
sva_svr:
diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml
index 78e2bcf26..5a9dfd9ab 100644
--- a/config/locales/models/procedure/fr.yml
+++ b/config/locales/models/procedure/fr.yml
@@ -8,7 +8,7 @@ fr:
procedure:
hints:
description: Décrivez en quelques lignes le contexte, la finalité, etc.
- description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les critères d’éligibilité s’il y en a, les pré-requis, etc.
+ description_target_audience: Décrivez en quelques lignes les destinataires finaux de la démarche, les conditions d’éligibilité s’il y en a, les pré-requis, etc.
description_pj: Décrivez la liste des pièces jointes à fournir s’il y en a
lien_site_web: "Il s'agit de la page de votre site web où le lien sera diffusé. Ex: https://exemple.gouv.fr/page_informant_sur_ma_demarche"
cadre_juridique: "Exemple: 'https://www.legifrance.gouv.fr/'"
@@ -78,8 +78,30 @@ fr:
invalid: 'n’a pas le bon format'
draft_types_de_champ_public:
format: 'Le champ %{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: "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:
+ format: "%{attribute} %{message}"
+ received_mail:
+ format: "%{attribute} %{message}"
+ closed_mail:
+ format: "%{attribute} %{message}"
+ refused_mail:
+ format: "%{attribute} %{message}"
+ without_continuation_mail:
+ format: "%{attribute} %{message}"
+ re_instructed_mail:
+ format: "%{attribute} %{message}"
lien_dpo:
invalid_uri_or_email: "Veuillez saisir un mail ou un lien"
auto_archive_on:
diff --git a/config/locales/models/procedure_revision/fr.yml b/config/locales/models/procedure_revision/fr.yml
new file mode 100644
index 000000000..1665415aa
--- /dev/null
+++ b/config/locales/models/procedure_revision/fr.yml
@@ -0,0 +1,7 @@
+fr:
+ activerecord:
+ attributes:
+ procedure_revision:
+ ineligibilite_message: Message d’inéligibilité
+ hints:
+ ineligibilite_message: "Ce message sera affiché à l’usager si son dossier est bloqué et lui expliquera la raison de son inéligibilité."
diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml
index a6ea09511..71de8c159 100644
--- a/config/locales/models/type_de_champ/fr.yml
+++ b/config/locales/models/type_de_champ/fr.yml
@@ -61,4 +61,4 @@ fr:
type_de_champ:
attributes:
header_section_level:
- gap_error: "Un titre de section avec le niveau %{level} est manquant."
+ gap_error: "devrait être précédé d'un titre de niveau %{level}"
diff --git a/config/locales/views/administrateurs/experts_procedures/fr.yml b/config/locales/views/administrateurs/experts_procedures/fr.yml
index 62db8096c..673b0a609 100644
--- a/config/locales/views/administrateurs/experts_procedures/fr.yml
+++ b/config/locales/views/administrateurs/experts_procedures/fr.yml
@@ -6,8 +6,11 @@ fr:
main: Experts invités sur %{libelle}
allow_invite_experts: "Autoriser les instructeurs à solliciter des experts invités"
allow_expert_messaging: "Autoriser les experts à accéder à la messagerie usager"
- manage_procedure_experts: "Gérer les experts invités de la démarche"
+ manage_procedure_experts: "Gérer les experts invités de la démarche avec une liste prédéfinie"
descriptions:
allow_invite_experts : Lorsque cette fonctionnalité est active, les instructeurs peuvent solliciter les experts
allow_expert_messaging: Lorsque cette fonctionnalité est active, les experts peuvent demander des informations aux usagers
manage_procedure_experts: Lorsque cette fonctionnalité est active, les instructeurs peuvent uniquement solliciter les experts de votre liste
+ experts_doc:
+ title: Avis externes documentation
+ url: 'https://app.gitbook.com/o/-L7_aClyGhmMzzsqtO4_/s/-L7_aKvpAJdAIEfxHudA/tutoriels/tutoriel-administrateur/~/comments?context=post&node=ff7bd481c1994d6aa56817b7237b9e59#id-12.-la-gestion-des-avis-experts-invites-de-votre-demarche'
diff --git a/config/routes.rb b/config/routes.rb
index c4973d1ae..d16b5f778 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -161,6 +161,7 @@ Rails.application.routes.draw do
end
get 'password_complexity' => 'password_complexity#show', as: 'show_password_complexity'
+ get 'check_email' => 'email_checker#show', as: 'show_email_suggestions'
resources :targeted_user_links, only: [:show]
@@ -607,6 +608,14 @@ Rails.application.routes.draw do
delete :delete_row, on: :member
end
+ resource :ineligibilite_rules, only: [:edit, :update, :destroy], param: :revision_id do
+ patch :change_targeted_champ, on: :member
+ patch :update_all_rows, on: :member
+ patch :add_row, on: :member
+ delete :delete_row, on: :member
+ patch :change
+ end
+
patch :update_defaut_groupe_instructeur, controller: 'routing_rules', as: :update_defaut_groupe_instructeur
put 'clone'
diff --git a/db/migrate/20240409075536_add_transitions_rules_to_procedure_revisions.rb b/db/migrate/20240409075536_add_transitions_rules_to_procedure_revisions.rb
new file mode 100644
index 000000000..e2a654783
--- /dev/null
+++ b/db/migrate/20240409075536_add_transitions_rules_to_procedure_revisions.rb
@@ -0,0 +1,5 @@
+class AddTransitionsRulesToProcedureRevisions < ActiveRecord::Migration[7.0]
+ def change
+ add_column :procedure_revisions, :ineligibilite_rules, :jsonb
+ end
+end
diff --git a/db/migrate/20240514075727_add_dossier_ineligble_message_to_procedure_revisions.rb b/db/migrate/20240514075727_add_dossier_ineligble_message_to_procedure_revisions.rb
new file mode 100644
index 000000000..bf8464f8c
--- /dev/null
+++ b/db/migrate/20240514075727_add_dossier_ineligble_message_to_procedure_revisions.rb
@@ -0,0 +1,5 @@
+class AddDossierIneligbleMessageToProcedureRevisions < ActiveRecord::Migration[7.0]
+ def change
+ add_column :procedure_revisions, :ineligibilite_message, :text
+ end
+end
diff --git a/db/migrate/20240516095601_add_eligibilite_dossiers_enabled_to_procedure_revisions.rb b/db/migrate/20240516095601_add_eligibilite_dossiers_enabled_to_procedure_revisions.rb
new file mode 100644
index 000000000..19ce1d243
--- /dev/null
+++ b/db/migrate/20240516095601_add_eligibilite_dossiers_enabled_to_procedure_revisions.rb
@@ -0,0 +1,5 @@
+class AddEligibiliteDossiersEnabledToProcedureRevisions < ActiveRecord::Migration[7.0]
+ def change
+ add_column :procedure_revisions, :ineligibilite_enabled, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6f7fcfe7c..b33a10582 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -863,6 +863,9 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_27_090508) do
create_table "procedure_revisions", force: :cascade do |t|
t.datetime "created_at", precision: nil, null: false
t.bigint "dossier_submitted_message_id"
+ t.boolean "ineligibilite_enabled", default: false, null: false
+ t.text "ineligibilite_message"
+ t.jsonb "ineligibilite_rules"
t.bigint "procedure_id", null: false
t.datetime "published_at", precision: nil
t.datetime "updated_at", precision: nil, null: false
diff --git a/package.json b/package.json
index 8d5144609..edad7ebc6 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,6 @@
"core-js": "^3.37.1",
"date-fns": "^2.30.0",
"debounce": "^1.2.1",
- "email-butler": "^1.0.13",
"geojson": "^0.5.0",
"graphiql": "^3.2.3",
"graphql": "^16.8.1",
diff --git a/spec/components/conditions/ineligibilite_rules_component_spec.rb b/spec/components/conditions/ineligibilite_rules_component_spec.rb
new file mode 100644
index 000000000..c678c5ace
--- /dev/null
+++ b/spec/components/conditions/ineligibilite_rules_component_spec.rb
@@ -0,0 +1,64 @@
+describe Conditions::IneligibiliteRulesComponent, type: :component do
+ include Logic
+ let(:procedure) { create(:procedure) }
+ let(:component) { described_class.new(draft_revision: procedure.draft_revision) }
+
+ describe 'render' do
+ let(:ineligibilite_message) { 'ok' }
+ let(:ineligibilite_enabled) { true }
+ before do
+ procedure.draft_revision.update(ineligibilite_rules:, ineligibilite_message:, ineligibilite_enabled:)
+ end
+ context 'when ineligibilite_rules are valid' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(true)) }
+ it 'does not render error' do
+ render_inline(component)
+ expect(page).not_to have_selector('.errors-summary')
+ end
+ end
+ context 'when ineligibilite_rules are invalid' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
+ it 'does not render error' do
+ render_inline(component)
+ expect(page).to have_selector('.errors-summary')
+ end
+ end
+ end
+
+ describe '#pending_changes' do
+ context 'when procedure is published' do
+ it 'detect changes when setup changes' do
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.draft_revision.ineligibilite_message = 'changed'
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_enabled = true
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_rules = {}
+ expect(component.pending_changes?).to be_falsey
+ end
+ end
+
+ context 'when procedure is published' do
+ let(:procedure) { create(:procedure, :published) }
+ it 'detect changes when setup changes' do
+ expect(component.pending_changes?).to be_falsey
+
+ procedure.draft_revision.ineligibilite_message = 'changed'
+ expect(component.pending_changes?).to be_truthy
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_enabled = true
+ expect(component.pending_changes?).to be_truthy
+
+ procedure.reload
+ procedure.draft_revision.ineligibilite_rules = {}
+ expect(component.pending_changes?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/components/dossiers/edit_footer_component_spec.rb b/spec/components/dossiers/edit_footer_component_spec.rb
new file mode 100644
index 000000000..4b8e1a77f
--- /dev/null
+++ b/spec/components/dossiers/edit_footer_component_spec.rb
@@ -0,0 +1,50 @@
+RSpec.describe Dossiers::EditFooterComponent, type: :component do
+ let(:annotation) { false }
+ let(:component) { Dossiers::EditFooterComponent.new(dossier:, annotation:) }
+
+ subject { render_inline(component).to_html }
+
+ before { allow(component).to receive(:owner?).and_return(true) }
+
+ context 'when brouillon' do
+ let(:dossier) { create(:dossier, :brouillon) }
+
+ context 'when dossier can be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(true) }
+ it 'renders submit button without disabled' do
+ expect(subject).to have_selector('button', text: 'Déposer le dossier')
+ end
+ end
+
+ context 'when dossier can not be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(false) }
+ it 'renders submit button with disabled' do
+ expect(subject).to have_selector('a', text: 'Pourquoi je ne peux pas déposer mon dossier ?')
+ expect(subject).to have_selector('button[disabled]', text: 'Déposer le dossier')
+ end
+ end
+ end
+
+ context 'when en construction' do
+ let(:fork_origin) { create(:dossier, :en_construction) }
+ let(:dossier) { fork_origin.clone(fork: true) }
+ before { allow(dossier).to receive(:forked_with_changes?).and_return(true) }
+
+ context 'when dossier can be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(true) }
+
+ it 'renders submit button without disabled' do
+ expect(subject).to have_selector('button', text: 'Déposer les modifications')
+ end
+ end
+
+ context 'when dossier can not be submitted' do
+ before { allow(component).to receive(:can_passer_en_construction?).and_return(false) }
+
+ it 'renders submit button with disabled' do
+ expect(subject).to have_selector('a', text: 'Pourquoi je ne peux pas déposer mon dossier ?')
+ expect(subject).to have_selector('button[disabled]', text: 'Déposer les modifications')
+ end
+ end
+ end
+end
diff --git a/spec/components/procedures/card/annotations_component_spec.rb b/spec/components/procedures/card/annotations_component_spec.rb
new file mode 100644
index 000000000..478937124
--- /dev/null
+++ b/spec/components/procedures/card/annotations_component_spec.rb
@@ -0,0 +1,30 @@
+describe Procedure::Card::AnnotationsComponent, type: :component do
+ describe 'render' do
+ let(:procedure) { create(:procedure, id: 1, types_de_champ_private:, types_de_champ_public:) }
+ let(:types_de_champ_private) { [] }
+ let(:types_de_champ_public) { [] }
+ before { procedure.validate(:publication) }
+ subject { render_inline(described_class.new(procedure: procedure)) }
+
+ context 'when no errors' do
+ it 'does not render' do
+ expect(subject).to have_selector('.fr-badge--info', text: 'À configurer')
+ end
+ end
+
+ context 'when errors on types_de_champs_public' do
+ let(:types_de_champ_public) { [{ type: :drop_down_list, options: [] }] }
+ it 'does not render' do
+ expect(subject).to have_selector('.fr-badge--info', text: 'À configurer')
+ end
+ end
+
+ context 'when errors on types_de_champs_private' do
+ let(:types_de_champ_private) { [{ type: :drop_down_list, options: [] }] }
+
+ it 'render the template' do
+ expect(subject).to have_selector('.fr-badge--error', text: 'À modifier')
+ end
+ end
+ end
+end
diff --git a/spec/components/procedures/card/champs_component_spec.rb b/spec/components/procedures/card/champs_component_spec.rb
new file mode 100644
index 000000000..61a5d5762
--- /dev/null
+++ b/spec/components/procedures/card/champs_component_spec.rb
@@ -0,0 +1,30 @@
+describe Procedure::Card::ChampsComponent, type: :component do
+ describe 'render' do
+ let(:procedure) { create(:procedure, id: 1, types_de_champ_private:, types_de_champ_public:) }
+ let(:types_de_champ_private) { [] }
+ let(:types_de_champ_public) { [] }
+ before { procedure.validate(:publication) }
+ subject { render_inline(described_class.new(procedure: procedure)) }
+
+ context 'when no errors' do
+ it 'does not render' do
+ expect(subject).to have_selector('.fr-badge--warning', text: 'À faire')
+ end
+ end
+
+ context 'when errors on types_de_champs_public' do
+ let(:types_de_champ_public) { [{ type: :drop_down_list, options: [] }] }
+ it 'does not render' do
+ expect(subject).to have_selector('.fr-badge--error', text: 'À modifier')
+ end
+ end
+
+ context 'when errors on types_de_champs_private' do
+ let(:types_de_champ_private) { [{ type: :drop_down_list, options: [] }] }
+
+ it 'render the template' do
+ expect(subject).to have_selector('.fr-badge--warning', text: 'À faire')
+ end
+ end
+ end
+end
diff --git a/spec/components/procedures/card/ineligibilite_dossier_component.rb b/spec/components/procedures/card/ineligibilite_dossier_component.rb
new file mode 100644
index 000000000..433b59155
--- /dev/null
+++ b/spec/components/procedures/card/ineligibilite_dossier_component.rb
@@ -0,0 +1,25 @@
+describe Procedure::Card::IneligibiliteDossierComponent, type: :component do
+ describe 'render' do
+ subject do
+ render_inline(described_class.new(procedure: procedure))
+ end
+
+ context 'when none of types_de_champ_public supports conditional' do
+ let(:procedure) { create(:procedure, types_de_champ_public: []) }
+
+ it 'render missing setup' do
+ subject
+ expect(page).to have_text('Champs manquant')
+ end
+ end
+
+ context 'when at least one of types_de_champ_public support conditional' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) }
+
+ it 'render the template' do
+ subject
+ expect(page).to have_text('À configurer')
+ end
+ end
+ end
+end
diff --git a/spec/components/procedures/errors_summary_spec.rb b/spec/components/procedures/errors_summary_spec.rb
new file mode 100644
index 000000000..ebeb1096d
--- /dev/null
+++ b/spec/components/procedures/errors_summary_spec.rb
@@ -0,0 +1,97 @@
+describe Procedure::ErrorsSummary, type: :component do
+ subject { render_inline(described_class.new(procedure:, validation_context:)) }
+
+ describe 'validations context' do
+ let(:procedure) { create(:procedure, types_de_champ_private:, types_de_champ_public:) }
+ let(:types_de_champ_private) { [{ type: :drop_down_list, options: [], libelle: 'private' }] }
+ let(:types_de_champ_public) { [{ type: :drop_down_list, options: [], libelle: 'public' }] }
+
+ before { subject }
+
+ context 'when :publication' do
+ let(:validation_context) { :publication }
+
+ 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 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 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
+
+ describe 'render all kind of champs errors' do
+ include Logic
+
+ let(:procedure) do
+ create(:procedure, id: 1, types_de_champ_public: [
+ { libelle: 'repetition requires children', type: :repetition, children: [] },
+ { libelle: 'drop down list requires options', type: :drop_down_list, options: [] },
+ { libelle: 'invalid condition', type: :text, condition: ds_eq(constant(true), constant(1)) },
+ { libelle: 'header sections must have consistent order', type: :header_section, level: 2 }
+ ])
+ end
+
+ let(:validation_context) { :types_de_champ_public_editor }
+
+ before { subject }
+
+ 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) }
+ let(:initiated_mail) { build(:initiated_mail) }
+
+ 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_selector("a", text: "Les règles d’inéligibilité")
+ expect(page).to have_selector("a", text: "Le modèle d’attestation")
+ expect(page).to have_selector("a", text: "L’email de notification de passage de dossier en instruction")
+ expect(page).to have_text("n'est pas valide", count: 2)
+ end
+ end
+end
diff --git a/spec/components/procedures/pending_republish_component_spec.rb b/spec/components/procedures/pending_republish_component_spec.rb
new file mode 100644
index 000000000..a5e301a20
--- /dev/null
+++ b/spec/components/procedures/pending_republish_component_spec.rb
@@ -0,0 +1,14 @@
+describe Procedure::PendingRepublishComponent, type: :component do
+ subject { render_inline(described_class.new(render_if:, procedure: build(:procedure, id: 1))) }
+ let(:page) { subject }
+ describe 'render_if' do
+ context 'when false' do
+ let(:render_if) { false }
+ it { expect(page).not_to have_text('Ces modifications ne seront appliquées') }
+ end
+ context 'when true' do
+ let(:render_if) { true }
+ it { expect(page).to have_text('Ces modifications ne seront appliquées') }
+ end
+ end
+end
diff --git a/spec/components/types_de_champ_editor/champ_component_spec.rb b/spec/components/types_de_champ_editor/champ_component_spec.rb
index 1e368f1ba..27b57472f 100644
--- a/spec/components/types_de_champ_editor/champ_component_spec.rb
+++ b/spec/components/types_de_champ_editor/champ_component_spec.rb
@@ -2,10 +2,12 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
describe 'render' do
let(:component) { described_class.new(coordinate:, upper_coordinates: []) }
let(:routing_rules_stable_ids) { [] }
+ let(:ineligibilite_rules_used?) { false }
before do
Flipper.enable_actor(:engagement_juridique_type_de_champ, procedure)
allow_any_instance_of(Procedure).to receive(:stable_ids_used_by_routing_rules).and_return(routing_rules_stable_ids)
+ allow_any_instance_of(ProcedureRevisionTypeDeChamp).to receive(:used_by_ineligibilite_rules?).and_return(ineligibilite_rules_used?)
render_inline(component)
end
@@ -29,6 +31,15 @@ describe TypesDeChampEditor::ChampComponent, type: :component do
expect(page).to have_text(/utilisé pour\nle routage/)
end
end
+
+ context 'drop down tdc used for ineligibilite_rules' do
+ let(:ineligibilite_rules_used?) { true }
+
+ it do
+ expect(page).to have_css("select[disabled=\"disabled\"]")
+ expect(page).to have_text(/l’eligibilité des dossiers/)
+ end
+ end
end
describe 'tdc ej' do
diff --git a/spec/components/types_de_champ_editor/editor_component_spec.rb b/spec/components/types_de_champ_editor/editor_component_spec.rb
new file mode 100644
index 000000000..fb7094983
--- /dev/null
+++ b/spec/components/types_de_champ_editor/editor_component_spec.rb
@@ -0,0 +1,28 @@
+describe TypesDeChampEditor::EditorComponent, type: :component do
+ let(:revision) { procedure.draft_revision }
+ let(:procedure) { create(:procedure, id: 1, types_de_champ_private:, types_de_champ_public:) }
+
+ let(:types_de_champ_private) { [{ type: :drop_down_list, options: [], libelle: 'private' }] }
+ let(:types_de_champ_public) { [{ type: :drop_down_list, options: [], libelle: 'public' }] }
+
+ describe 'render' do
+ subject { render_inline(described_class.new(revision:, is_annotation:)) }
+ 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")
+ 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_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
+end
diff --git a/spec/controllers/administrateurs/ineligibilite_rules_controller_spec.rb b/spec/controllers/administrateurs/ineligibilite_rules_controller_spec.rb
new file mode 100644
index 000000000..2a76a054a
--- /dev/null
+++ b/spec/controllers/administrateurs/ineligibilite_rules_controller_spec.rb
@@ -0,0 +1,231 @@
+describe Administrateurs::IneligibiliteRulesController, type: :controller do
+ include Logic
+ let(:user) { create(:user) }
+ let(:admin) { create(:administrateur, user: create(:user)) }
+ let(:procedure) { create(:procedure, administrateurs: [admin], types_de_champ_public:) }
+ let(:types_de_champ_public) { [] }
+
+ describe 'condition management' do
+ before { sign_in(admin.user) }
+
+ let(:default_params) do
+ {
+ procedure_id: procedure.id,
+ revision_id: procedure.draft_revision.id
+ }
+ end
+
+ describe '#add_row' do
+ subject { post :add_row, params: default_params, format: :turbo_stream }
+
+ context 'without any row' do
+ it 'creates an empty condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(empty_operator(empty, empty))
+ end
+ end
+
+ context 'with row' do
+ before do
+ procedure.draft_revision.ineligibilite_rules = empty_operator(empty, empty)
+ procedure.draft_revision.save!
+ end
+
+ it 'add one more creates an empty condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(empty_operator(empty, empty))
+ .to(ds_and([
+ empty_operator(empty, empty),
+ empty_operator(empty, empty)
+ ]))
+ end
+ end
+ end
+
+ describe 'delete_row' do
+ let(:condition_form) do
+ {
+ top_operator_name: Logic::And.name,
+ rows: [
+ {
+ targeted_champ: empty.to_json,
+ operator_name: Logic::EmptyOperator,
+ value: empty.to_json
+ },
+ {
+ targeted_champ: empty.to_json,
+ operator_name: Logic::EmptyOperator,
+ value: empty.to_json
+ }
+ ]
+ }
+ end
+ let(:initial_condition) do
+ ds_and([
+ empty_operator(empty, empty),
+ empty_operator(empty, empty)
+ ])
+ end
+
+ subject { delete :delete_row, params: default_params.merge(row_index: 0, procedure_revision: { condition_form: }), format: :turbo_stream }
+ it 'remove condition' do
+ procedure.draft_revision.update(ineligibilite_rules: initial_condition)
+
+ expect { subject }
+ .to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(initial_condition)
+ .to(empty_operator(empty, empty))
+ end
+ end
+
+ context 'simple tdc' do
+ let(:types_de_champ_public) { [{ type: :yes_no }] }
+ let(:yes_no_tdc) { procedure.draft_revision.types_de_champ_for(scope: :public).first }
+ let(:targeted_champ) { champ_value(yes_no_tdc.stable_id).to_json }
+
+ describe '#change_targeted_champ' do
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: Logic::Eq.name,
+ value: constant(true).to_json
+ }
+ ]
+ }
+ end
+ subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
+ it 'update condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+
+ describe '#update' do
+ let(:value) { constant(true).to_json }
+ let(:operator_name) { Logic::Eq.name }
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: operator_name,
+ value: value
+ }
+ ]
+ }
+ end
+ subject { patch :update, params: default_params.merge(procedure_revision: { condition_form: condition_form }), format: :turbo_stream }
+ it 'updates condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+ end
+
+ context 'repetition tdc' do
+ let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :yes_no }] }] }
+ let(:yes_no_tdc) { procedure.draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'yes_no' } }
+ let(:targeted_champ) { champ_value(yes_no_tdc.stable_id).to_json }
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: Logic::Eq.name,
+ value: constant(true).to_json
+ }
+ ]
+ }
+ end
+ subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
+ describe "#update" do
+ it 'update condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+
+ describe '#change_targeted_champ' do
+ let(:condition_form) do
+ {
+ rows: [
+ {
+ targeted_champ: targeted_champ,
+ operator_name: Logic::Eq.name,
+ value: constant(true).to_json
+ }
+ ]
+ }
+ end
+ subject { patch :change_targeted_champ, params: default_params.merge(procedure_revision: { condition_form: }), format: :turbo_stream }
+ it 'update condition' do
+ expect { subject }.to change { procedure.draft_revision.reload.ineligibilite_rules }
+ .from(nil)
+ .to(ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+ end
+ end
+ end
+
+ describe '#edit' do
+ subject { get :edit, params: { procedure_id: procedure.id } }
+
+ context 'when user is not signed in' do
+ it { is_expected.to redirect_to(new_user_session_path) }
+ end
+
+ context 'when user is signed in but not admin of procedure' do
+ before { sign_in(user) }
+ it { is_expected.to redirect_to(new_user_session_path) }
+ end
+
+ context 'when user is signed as admin' do
+ before do
+ sign_in(admin.user)
+ subject
+ end
+
+ it { is_expected.to have_http_status(200) }
+
+ context 'rendered without tdc' do
+ let(:types_de_champ_public) { [] }
+ render_views
+
+ it { expect(response.body).to have_link("Ajouter un champ supportant les conditions d’inéligibilité") }
+ end
+
+ context 'rendered with tdc' do
+ let(:types_de_champ_public) { [{ type: :yes_no }] }
+ render_views
+
+ it { expect(response.body).not_to have_link("Ajouter un champ supportant les conditions d’inéligibilité") }
+ end
+ end
+ end
+
+ describe 'change' do
+ let(:params) do
+ {
+ procedure_id: procedure.id,
+ procedure_revision: {
+ ineligibilite_message: 'panpan',
+ ineligibilite_enabled: '1'
+ }
+ }
+ end
+ before { sign_in(admin.user) }
+ it 'works' do
+ patch :change, params: params
+ draft_revision = procedure.reload.draft_revision
+ expect(draft_revision.ineligibilite_message).to eq('panpan')
+ expect(draft_revision.ineligibilite_enabled).to eq(true)
+ expect(response).to redirect_to(edit_admin_procedure_ineligibilite_rules_path(procedure))
+ end
+ end
+end
diff --git a/spec/controllers/administrateurs/procedures_controller_spec.rb b/spec/controllers/administrateurs/procedures_controller_spec.rb
index e19155b82..116db94d1 100644
--- a/spec/controllers/administrateurs/procedures_controller_spec.rb
+++ b/spec/controllers/administrateurs/procedures_controller_spec.rb
@@ -16,24 +16,28 @@ describe Administrateurs::ProceduresController, type: :controller do
let(:tags) { "[\"planete\",\"environnement\"]" }
describe '#apercu' do
- render_views
-
- let(:procedure) { create(:procedure, :with_all_champs) }
-
subject { get :apercu, params: { id: procedure.id } }
before do
sign_in(admin.user)
end
- it do
- subject
- expect(response).to have_http_status(:ok)
- expect(procedure.dossiers.visible_by_user).to be_empty
- expect(procedure.dossiers.for_procedure_preview).not_to be_empty
+ context 'all tdc can be rendered' do
+ render_views
+
+ let(:procedure) { create(:procedure, :with_all_champs) }
+
+ it do
+ subject
+ expect(response).to have_http_status(:ok)
+ expect(procedure.dossiers.visible_by_user).to be_empty
+ expect(procedure.dossiers.for_procedure_preview).not_to be_empty
+ end
end
context 'when the draft is invalid' do
+ let(:procedure) { create(:procedure) }
+
before do
allow_any_instance_of(ProcedureRevision).to receive(:invalid?).and_return(true)
end
@@ -91,9 +95,6 @@ describe Administrateurs::ProceduresController, type: :controller do
let!(:draft_procedure) { create(:procedure) }
let!(:published_procedure) { create(:procedure_with_dossiers, :published, dossiers_count: 2) }
let!(:closed_procedure) { create(:procedure, :closed) }
- let!(:procedure_detail_draft) { ProcedureDetail.new(id: draft_procedure.id, latest_zone_labels: '{ "zone1", "zone2" }') }
- let!(:procedure_detail_published) { ProcedureDetail.new(id: published_procedure.id, latest_zone_labels: '{ "zone3", "zone4" }') }
- let!(:procedure_detail_closed) { ProcedureDetail.new(id: closed_procedure.id, latest_zone_labels: '{ "zone5", "zone6" }') }
subject { get :all }
@@ -120,19 +121,6 @@ describe Administrateurs::ProceduresController, type: :controller do
expect(assigns(:procedures).any? { |p| p.id == draft_procedure.id }).to be_falsey
end
- context 'with parsed latest zone labels' do
- it 'parses the latest zone labels correctly' do
- expect(procedure_detail_draft.parsed_latest_zone_labels).to eq(["zone1", "zone2"])
- expect(procedure_detail_published.parsed_latest_zone_labels).to eq(["zone3", "zone4"])
- expect(procedure_detail_closed.parsed_latest_zone_labels).to eq(["zone5", "zone6"])
- end
-
- it 'returns an empty array for invalid JSON' do
- procedure_detail_draft.latest_zone_labels = '{ invalid json }'
- expect(procedure_detail_draft.parsed_latest_zone_labels).to eq([])
- end
- end
-
context 'for default admin zones' do
let(:zone1) { create(:zone) }
let(:zone2) { create(:zone) }
diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb
index 6fa77c40b..a42d90210 100644
--- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb
+++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb
@@ -912,6 +912,17 @@ describe API::V2::GraphqlController do
expect(ActionMailer::Base.deliveries.size).to eq(0)
}
end
+
+ context 'with pending corrections' do
+ before { Flipper.enable(:blocking_pending_correction, dossier.procedure) }
+ let!(:dossier_correction) { create(:dossier_correction, dossier:) }
+
+ it {
+ expect(dossier.pending_correction?).to be_truthy
+ expect(gql_errors).to be_nil
+ expect(gql_data[:dossierPasserEnInstruction][:errors]).to eq([{ message: "Le dossier est en attente de correction" }])
+ }
+ end
end
context 'dossierRepasserEnConstruction' do
@@ -1332,5 +1343,77 @@ describe API::V2::GraphqlController do
}
end
end
+
+ context 'dossierEnvoyerMessage' do
+ let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure:) }
+ let(:variables) { { input: { dossierId: dossier.to_typed_id, instructeurId: instructeur.to_typed_id, body: 'Hello World!' } } }
+ let(:operation_name) { 'dossierEnvoyerMessage' }
+
+ it {
+ expect(gql_errors).to be_nil
+ expect(gql_data[:dossierEnvoyerMessage][:errors]).to be_nil
+ expect(gql_data[:dossierEnvoyerMessage][:message][:id]).to eq(dossier.commentaires.first.to_typed_id)
+ perform_enqueued_jobs
+ expect(ActionMailer::Base.deliveries.size).to eq(1)
+ }
+ end
+
+ context 'dossierSupprimerMessage' do
+ let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure:) }
+ let(:message) { create(:commentaire, dossier:, instructeur:) }
+ let(:dossier_correction) { create(:dossier_correction, dossier:, commentaire: message) }
+ let(:variables) { { input: { messageId: message.to_typed_id, instructeurId: instructeur.to_typed_id } } }
+ let(:operation_name) { 'dossierSupprimerMessage' }
+
+ it {
+ expect(message.discarded?).to be_falsey
+ expect(gql_errors).to be_nil
+ expect(gql_data[:dossierSupprimerMessage][:errors]).to be_nil
+ expect(gql_data[:dossierSupprimerMessage][:message][:id]).to eq(message.to_typed_id)
+ expect(gql_data[:dossierSupprimerMessage][:message][:discardedAt]).not_to be_nil
+ expect(message.reload.discarded?).to be_truthy
+ }
+
+ it {
+ expect(dossier_correction.commentaire.discarded?).to be_falsey
+ expect(dossier.pending_correction?).to be_truthy
+ expect(gql_errors).to be_nil
+ expect(gql_data[:dossierSupprimerMessage][:errors]).to be_nil
+ expect(gql_data[:dossierSupprimerMessage][:message][:id]).to eq(message.to_typed_id)
+ expect(gql_data[:dossierSupprimerMessage][:message][:discardedAt]).not_to be_nil
+ expect(message.reload.discarded?).to be_truthy
+ expect(dossier.pending_correction?).to be_falsey
+ }
+
+ context 'when unauthorized' do
+ let(:dossier) { create(:dossier, :en_construction, :with_individual) }
+
+ it {
+ expect(message.discarded?).to be_falsey
+ expect(gql_errors.first[:message]).to eq("An object of type Message was hidden due to permissions")
+ }
+ end
+
+ context 'when from not the same instructeur' do
+ let(:other_instructeur) { create(:instructeur, followed_dossiers: dossiers) }
+ let(:variables) { { input: { messageId: message.to_typed_id, instructeurId: other_instructeur.to_typed_id } } }
+
+ it {
+ expect(message.discarded?).to be_falsey
+ expect(gql_errors).to be_nil
+ expect(gql_data[:dossierSupprimerMessage][:errors]).to eq([{ message: "Le message ne peut pas être supprimé" }])
+ }
+ end
+
+ context 'when from usager' do
+ let(:message) { create(:commentaire, dossier:) }
+
+ it {
+ expect(message.discarded?).to be_falsey
+ expect(gql_errors).to be_nil
+ expect(gql_data[:dossierSupprimerMessage][:errors]).to eq([{ message: "Le message ne peut pas être supprimé" }])
+ }
+ end
+ end
end
end
diff --git a/spec/controllers/email_checker_controller_spec.rb b/spec/controllers/email_checker_controller_spec.rb
new file mode 100644
index 000000000..4572c2cd4
--- /dev/null
+++ b/spec/controllers/email_checker_controller_spec.rb
@@ -0,0 +1,39 @@
+describe EmailCheckerController, type: :controller do
+ describe '#show' do
+ render_views
+ before { get :show, format: :json, params: params }
+ let(:body) { JSON.parse(response.body, symbolize_names: true) }
+
+ context 'valid email' do
+ let(:params) { { email: 'martin@orange.fr' } }
+ it do
+ expect(response).to have_http_status(:success)
+ expect(body).to eq({ success: true })
+ end
+ end
+
+ context 'email with typo' do
+ let(:params) { { email: 'martin@orane.fr' } }
+ it do
+ expect(response).to have_http_status(:success)
+ expect(body).to eq({ success: true, email_suggestions: ['martin@orange.fr'] })
+ end
+ end
+
+ context 'empty' do
+ let(:params) { { email: '' } }
+ it do
+ expect(response).to have_http_status(:success)
+ expect(body).to eq({ success: false })
+ end
+ end
+
+ context 'notanemail' do
+ let(:params) { { email: 'clarkkent' } }
+ it do
+ expect(response).to have_http_status(:success)
+ expect(body).to eq({ success: false })
+ end
+ end
+ end
+end
diff --git a/spec/controllers/instructeurs/export_templates_controller_spec.rb b/spec/controllers/instructeurs/export_templates_controller_spec.rb
index 8b8d73b82..36c9f665e 100644
--- a/spec/controllers/instructeurs/export_templates_controller_spec.rb
+++ b/spec/controllers/instructeurs/export_templates_controller_spec.rb
@@ -21,26 +21,45 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
{ "type" => "paragraph", "content" => [{ "text" => "DOSSIER_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }
]
}.to_json,
- "pjs" =>
- [
- { path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " _justif", "type" => "text" }] }] }, stable_id: "3" },
- {
- path:
- { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "cni_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] },
- stable_id: "5"
- },
- {
- path: { "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "text" => "pj_repet_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }, { "text" => " ", "type" => "text" }] }] },
- stable_id: "10"
- }
- ]
+ tiptap_pj_3: {
+ "type" => "doc",
+ "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
+ }.to_json,
+ tiptap_pj_5: {
+
+ "type" => "doc",
+ "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
+ }.to_json,
+ tiptap_pj_10: {
+
+ "type" => "doc",
+ "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
+ }.to_json
}
end
let(:instructeur) { create(:instructeur) }
- let(:procedure) { create(:procedure, instructeurs: [instructeur]) }
+ let(:procedure) do
+ create(
+ :procedure, instructeurs: [instructeur],
+ types_de_champ_public: [
+ { type: :piece_justificative, libelle: "pj1", stable_id: 3 },
+ { type: :piece_justificative, libelle: "pj2", stable_id: 5 },
+ { type: :piece_justificative, libelle: "pj3", stable_id: 10 }
+ ]
+ )
+ end
let(:groupe_instructeur) { procedure.defaut_groupe_instructeur }
+ describe '#new' do
+ let(:subject) { get :new, params: { procedure_id: procedure.id } }
+
+ it do
+ subject
+ expect(assigns(:export_template)).to be_present
+ end
+ end
+
describe '#create' do
let(:subject) { post :create, params: { procedure_id: procedure.id, export_template: export_template_params } }
@@ -130,4 +149,19 @@ describe Instructeurs::ExportTemplatesController, type: :controller do
end
end
end
+
+ describe '#preview' do
+ render_views
+
+ let(:export_template) { create(:export_template, groupe_instructeur:) }
+
+ let(:subject) { get :preview, params: { procedure_id: procedure.id, id: export_template.id, export_template: export_template_params }, format: :turbo_stream }
+
+ it '' do
+ dossier = create(:dossier, procedure: procedure, for_procedure_preview: true)
+ subject
+ expect(response.body).to include "DOSSIER_#{dossier.id}"
+ expect(response.body).to include "mon_export_#{dossier.id}.pdf"
+ end
+ end
end
diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb
index 8d3b702f9..2a38eb7d7 100644
--- a/spec/controllers/users/dossiers_controller_spec.rb
+++ b/spec/controllers/users/dossiers_controller_spec.rb
@@ -398,7 +398,9 @@ describe Users::DossiersController, type: :controller do
describe '#submit_brouillon' do
before { sign_in(user) }
- let!(:dossier) { create(:dossier, user: user) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :text }] }
+ let!(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:anchor_to_first_champ) { controller.helpers.link_to first_champ.libelle, brouillon_dossier_path(anchor: first_champ.labelledby_id), class: 'error-anchor' }
let(:value) { 'beautiful value' }
@@ -439,9 +441,9 @@ describe Users::DossiersController, type: :controller do
render_views
let(:error_message) { 'nop' }
before do
- expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
- expect_any_instance_of(Dossier).to receive(:errors).and_return(
- [double(inner_error: double(base: first_champ), message: 'nop')]
+ allow_any_instance_of(Dossier).to receive(:validate).and_return(false)
+ allow_any_instance_of(Dossier).to receive(:errors).and_return(
+ [instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')]
)
subject
end
@@ -461,11 +463,8 @@ describe Users::DossiersController, type: :controller do
render_views
let(:value) { nil }
-
- before do
- first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
- subject
- end
+ let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] }
+ before { subject }
it { expect(response).to render_template(:brouillon) }
it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") }
@@ -548,8 +547,8 @@ describe Users::DossiersController, type: :controller do
render_views
before do
- expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
- expect_any_instance_of(Dossier).to receive(:errors).and_return(
+ allow_any_instance_of(Dossier).to receive(:validate).and_return(false)
+ allow_any_instance_of(Dossier).to receive(:errors).and_return(
[double(inner_error: double(base: first_champ), message: 'nop')]
)
@@ -661,7 +660,8 @@ describe Users::DossiersController, type: :controller do
describe '#update brouillon' do
before { sign_in(user) }
- let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}, { type: :piece_justificative }]) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{}, { type: :piece_justificative }] }
let(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:piece_justificative_champ) { dossier.champs_public.last }
@@ -754,13 +754,66 @@ describe Users::DossiersController, type: :controller do
end
end
- it "debounce search terms indexation" do
- # dossier creation trigger a first indexation and flag,
- # so we we have to remove this flag
- dossier.debounce_index_search_terms_flag.remove
+ context 'having ineligibilite_rules setup' do
+ include Logic
+ render_views
- assert_enqueued_jobs(1, only: DossierIndexSearchTermsJob) do
- 3.times { patch :update, params: payload, format: :turbo_stream }
+ let(:types_de_champ_public) { [{ type: :text }, { type: :integer_number }] }
+ let(:text_champ) { dossier.champs_public.first }
+ let(:number_champ) { dossier.champs_public.last }
+ let(:submit_payload) do
+ {
+ id: dossier.id,
+ dossier: {
+ groupe_instructeur_id: dossier.groupe_instructeur_id,
+ champs_public_attributes: {
+ text_champ.public_id => {
+ with_public_id: true,
+ value: "hello world"
+ },
+ number_champ.public_id => {
+ with_public_id: true,
+ value:
+ }
+ }
+ }
+ }
+ end
+ let(:must_be_greater_than) { 10 }
+
+ before do
+ procedure.published_revision.update(
+ ineligibilite_enabled: true,
+ ineligibilite_message: 'lol',
+ ineligibilite_rules: greater_than(champ_value(number_champ.stable_id), constant(must_be_greater_than))
+ )
+ procedure.published_revision.save!
+ end
+ render_views
+
+ context 'when it switches from true to false' do
+ let(:value) { must_be_greater_than + 1 }
+
+ it 'raises popup' do
+ subject
+ dossier.reload
+ expect(dossier.can_passer_en_construction?).to be_falsey
+ expect(assigns(:can_passer_en_construction_was)).to eq(true)
+ expect(assigns(:can_passer_en_construction_is)).to eq(false)
+ expect(response.body).to match(ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken))
+ end
+ end
+
+ context 'when it stays true' do
+ let(:value) { must_be_greater_than - 1 }
+ it 'does nothing' do
+ subject
+ dossier.reload
+ expect(dossier.can_passer_en_construction?).to be_truthy
+ expect(assigns(:can_passer_en_construction_was)).to eq(true)
+ expect(assigns(:can_passer_en_construction_is)).to eq(true)
+ expect(response.body).not_to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}")
+ end
end
end
end
@@ -868,8 +921,8 @@ describe Users::DossiersController, type: :controller do
context 'classic error' do
before do
- expect_any_instance_of(Dossier).to receive(:save).and_return(false)
- expect_any_instance_of(Dossier).to receive(:errors).and_return(
+ allow_any_instance_of(Dossier).to receive(:save).and_return(false)
+ allow_any_instance_of(Dossier).to receive(:errors).and_return(
[message: 'nop', inner_error: double(base: first_champ)]
)
subject
@@ -1489,15 +1542,18 @@ describe Users::DossiersController, type: :controller do
end
describe '#clone' do
- let(:procedure) { create(:procedure, :with_all_champs) }
let(:dossier) { create(:dossier, procedure: procedure) }
subject { post :clone, params: { id: dossier.id } }
context 'not signed in' do
+ let(:procedure) { create(:procedure) }
+
it { expect(subject).to redirect_to(new_user_session_path) }
end
context 'signed with user dossier' do
+ let(:procedure) { create(:procedure, :with_all_champs) }
+
before { sign_in dossier.user }
it { expect(subject).to redirect_to(brouillon_dossier_path(Dossier.last)) }
diff --git a/spec/factories/export_template.rb b/spec/factories/export_template.rb
index 0f4e8d882..6785356af 100644
--- a/spec/factories/export_template.rb
+++ b/spec/factories/export_template.rb
@@ -11,24 +11,78 @@ FactoryBot.define do
{ "type" => "paragraph", "content" => [{ "text" => "export_", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_id", "label" => "id dossier" } }, { "text" => " .pdf", "type" => "text" }] }
]
},
- "default_dossier_directory" =>
- {
- "type" => "doc",
- "content" =>
- [
- {
- "type" => "paragraph",
- "content" =>
- [
- { "text" => "dossier_", "type" => "text" },
- { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
- { "text" => " ", "type" => "text" }
- ]
+ "default_dossier_directory" => {
+ "type" => "doc",
+ "content" =>
+ [
+ {
+ "type" => "paragraph",
+ "content" =>
+ [
+ { "text" => "dossier_", "type" => "text" },
+ { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
+ { "text" => " ", "type" => "text" }
+ ]
+ }
+ ]
}
- ]
- }
}
}
kind { "zip" }
+
+ to_create do |export_template, _context|
+ export_template.set_default_values
+ export_template.save
+ end
+
+ trait :with_custom_content do
+ to_create do |export_template, context|
+ export_template.set_default_values
+ export_template.content = context.content
+ export_template.save
+ end
+ end
+
+ trait :with_custom_ddd_prefix do
+ transient do
+ ddd_prefix { 'dossier_' }
+ end
+
+ to_create do |export_template, context|
+ export_template.set_default_values
+ export_template.content["default_dossier_directory"]["content"] = [
+ {
+ "type" => "paragraph",
+ "content" =>
+ [
+ { "text" => context.ddd_prefix, "type" => "text" },
+ { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
+ { "text" => " ", "type" => "text" }
+ ]
+ }
+ ]
+ export_template.save
+ end
+ end
+
+ trait :with_date_depot_for_export_pdf do
+ to_create do |export_template, _|
+ export_template.set_default_values
+ export_template.content["pdf_name"]["content"] = [
+ {
+ "type" => "paragraph",
+ "content" =>
+ [
+ { "text" => "export_", "type" => "text" },
+ { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } },
+ { "text" => "-", "type" => "text" },
+ { "type" => "mention", "attrs" => { "id" => "dossier_depose_at", "label" => "date de dépôt" } },
+ { "text" => " ", "type" => "text" }
+ ]
+ }
+ ]
+ export_template.save
+ end
+ end
end
end
diff --git a/spec/fixtures/files/pencil.tiff b/spec/fixtures/files/pencil.tiff
new file mode 100644
index 000000000..67af5a81a
Binary files /dev/null and b/spec/fixtures/files/pencil.tiff differ
diff --git a/spec/helpers/gallery_helper_spec.rb b/spec/helpers/gallery_helper_spec.rb
new file mode 100644
index 000000000..41469798c
--- /dev/null
+++ b/spec/helpers/gallery_helper_spec.rb
@@ -0,0 +1,57 @@
+RSpec.describe GalleryHelper, type: :helper do
+ let(:procedure) { create(:procedure_with_dossiers) }
+ let(:type_de_champ_pj) { create(:type_de_champ_piece_justificative, stable_id: 3, libelle: 'Justificatif de domicile', procedure:) }
+ let(:champ_pj) { create(:champ_piece_justificative, type_de_champ: type_de_champ_pj) }
+ let(:blob_info) do
+ {
+ filename: file.original_filename,
+ byte_size: file.size,
+ checksum: Digest::SHA256.file(file.path),
+ content_type: file.content_type,
+ # we don't want to run virus scanner on this file
+ metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
+ }
+ end
+ let(:blob) do
+ blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_info)
+ blob.upload(file)
+ blob
+ end
+ let(:attachment) { ActiveStorage::Attachment.create(name: "test", blob: blob, record: champ_pj) }
+
+ describe ".variant_url_for" do
+ subject { variant_url_for(attachment) }
+
+ context "when attachment can be represented with a variant" do
+ let(:file) { fixture_file_upload('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
+
+ it { expect { subject }.to change { ActiveStorage::VariantRecord.count }.by(1) }
+ it { is_expected.not_to eq("apercu-indisponible.png") }
+ end
+
+ context "when attachment cannot be represented with a variant" do
+ let(:file) { fixture_file_upload('spec/fixtures/files/instructeurs-file.csv', 'text/csv') }
+
+ it { expect { subject }.not_to change { ActiveStorage::VariantRecord.count } }
+ it { is_expected.to eq("apercu-indisponible.png") }
+ end
+ end
+
+ describe ".preview_url_for" do
+ subject { preview_url_for(attachment) }
+
+ context "when attachment can be represented with a preview" do
+ let(:file) { fixture_file_upload('spec/fixtures/files/RIB.pdf', 'application/pdf') }
+
+ it { expect { subject }.to change { ActiveStorage::VariantRecord.count }.by(1) }
+ it { is_expected.not_to eq("pdf-placeholder.png") }
+ end
+
+ context "when attachment cannot be represented with a preview" do
+ let(:file) { fixture_file_upload('spec/fixtures/files/instructeurs-file.csv', 'text/csv') }
+
+ it { expect { subject }.not_to change { ActiveStorage::VariantRecord.count } }
+ it { is_expected.to eq("pdf-placeholder.png") }
+ end
+ end
+end
diff --git a/spec/jobs/image_processor_job_spec.rb b/spec/jobs/image_processor_job_spec.rb
index e32999273..91d7de2c1 100644
--- a/spec/jobs/image_processor_job_spec.rb
+++ b/spec/jobs/image_processor_job_spec.rb
@@ -63,7 +63,6 @@ describe ImageProcessorJob, type: :job do
end
describe 'create representation' do
- let(:file) { fixture_file_upload('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
let(:blob_info) do
{
filename: file.original_filename,
@@ -81,19 +80,35 @@ describe ImageProcessorJob, type: :job do
blob
end
- context "when representation is not required" do
- it "it does not create blob representation" do
- expect { described_class.perform_now(blob) }.not_to change { ActiveStorage::VariantRecord.count }
+ context "when type image is usual" do
+ let(:file) { fixture_file_upload('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
+
+ context "when representation is not required" do
+ it "it does not create blob representation" do
+ expect { described_class.perform_now(blob) }.not_to change { ActiveStorage::VariantRecord.count }
+ end
+ end
+
+ context "when representation is required" do
+ before do
+ allow(blob).to receive(:representation_required?).and_return(true)
+ end
+
+ it "it creates blob representation" do
+ expect { described_class.perform_now(blob) }.to change { ActiveStorage::VariantRecord.count }.by(1)
+ end
end
end
- context "when representation is required" do
+ context "when type image is rare" do
+ let(:file) { fixture_file_upload('spec/fixtures/files/pencil.tiff', 'image/tiff') }
+
before do
allow(blob).to receive(:representation_required?).and_return(true)
end
- it "it creates blob representation" do
- expect { described_class.perform_now(blob) }.to change { ActiveStorage::VariantRecord.count }.by(1)
+ it "creates a second variant" do
+ expect { described_class.perform_now(blob) }.to change { ActiveStorage::VariantRecord.count }.by(2)
end
end
end
diff --git a/spec/lib/balancer_delivery_method_spec.rb b/spec/lib/balancer_delivery_method_spec.rb
index 659adb46e..92cb80082 100644
--- a/spec/lib/balancer_delivery_method_spec.rb
+++ b/spec/lib/balancer_delivery_method_spec.rb
@@ -2,8 +2,8 @@ RSpec.describe BalancerDeliveryMethod do
class ExampleMailer < ApplicationMailer
include BalancedDeliveryConcern
- def greet(name, bypass_unverified_mail_protection: true)
- mail(to: name, from: "smtp_from", body: "Hello #{name}")
+ def greet(name, bypass_unverified_mail_protection: true, **mail_args)
+ mail(to: name, from: "smtp_from", body: "Hello #{name}", **mail_args)
bypass_unverified_mail_protection! if bypass_unverified_mail_protection
end
@@ -202,6 +202,13 @@ RSpec.describe BalancerDeliveryMethod do
it { expect(mail).to have_been_delivered_using(MockSmtp) }
end
end
+
+ context 'when there are only bcc recipients' do
+ let(:bypass_unverified_mail_protection) { false }
+ let(:mail) { ExampleMailer.greet(nil, bypass_unverified_mail_protection: false, bcc: ["'u@a.com'"]) }
+
+ it { expect(mail).to have_been_delivered_using(MockSmtp) }
+ end
end
# Helpers
diff --git a/spec/lib/email_checker_spec.rb b/spec/lib/email_checker_spec.rb
new file mode 100644
index 000000000..f9c35ea91
--- /dev/null
+++ b/spec/lib/email_checker_spec.rb
@@ -0,0 +1,36 @@
+describe EmailChecker do
+ describe 'check' do
+ subject { described_class.new }
+
+ it 'works with identified use cases' do
+ expect(subject.check(email: nil)).to eq({ success: false })
+ expect(subject.check(email: '')).to eq({ success: false })
+ expect(subject.check(email: 'panpan')).to eq({ success: false })
+
+ # allow same domain
+ expect(subject.check(email: "martin@orange.fr")).to eq({ success: true })
+ # find difference of 1 lev distance
+ expect(subject.check(email: "martin@orane.fr")).to eq({ success: true, email_suggestions: ['martin@orange.fr'] })
+ # find difference of 2 lev distance, only with same chars
+ expect(subject.check(email: "martin@oragne.fr")).to eq({ success: true, email_suggestions: ['martin@orange.fr'] })
+ # ignore unknown domain
+ expect(subject.check(email: "martin@ore.fr")).to eq({ success: true })
+ end
+
+ it 'passes through real use cases, with levenshtein_distance 1' do
+ expect(subject.check(email: "martin@asn.com")).to eq({ success: true, email_suggestions: ['martin@msn.com'] })
+ expect(subject.check(email: "martin@gamail.com")).to eq({ success: true, email_suggestions: ['martin@gmail.com'] })
+ expect(subject.check(email: "martin@glail.com")).to eq({ success: true, email_suggestions: ['martin@gmail.com'] })
+ expect(subject.check(email: "martin@gmail.coml")).to eq({ success: true, email_suggestions: ['martin@gmail.com'] })
+ expect(subject.check(email: "martin@gmail.con")).to eq({ success: true, email_suggestions: ['martin@gmail.com'] })
+ expect(subject.check(email: "martin@hotmil.fr")).to eq({ success: true, email_suggestions: ['martin@hotmail.fr'] })
+ expect(subject.check(email: "martin@mail.com")).to eq({ success: true, email_suggestions: ["martin@gmail.com", "martin@ymail.com", "martin@mailo.com"] })
+ expect(subject.check(email: "martin@msc.com")).to eq({ success: true, email_suggestions: ["martin@msn.com", "martin@mac.com"] })
+ expect(subject.check(email: "martin@ymail.com")).to eq({ success: true })
+ end
+
+ it 'passes through real use cases, with levenshtein_distance 2, must share all chars' do
+ expect(subject.check(email: "martin@oise.fr")).to eq({ success: true }) # could be live.fr
+ end
+ end
+end
diff --git a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb
deleted file mode 100644
index 1a4fdf386..000000000
--- a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-describe '20220705164551_remove_unused_champs' do
- let(:rake_task) { Rake::Task['after_party:remove_unused_champs'] }
- let(:procedure) { create(:procedure, :with_all_champs) }
- let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) }
- let(:champ_repetition) { dossier.champs_public.find(&:repetition?) }
-
- subject(:run_task) do
- dossier
- rake_task.invoke
- end
-
- before { champ_repetition.champs.first.update(type_de_champ: create(:type_de_champ)) }
- after { rake_task.reenable }
-
- describe 'remove_unused_champs' do
- it "with bad champs" do
- expect(Champ.where(dossier: dossier).count).to eq(44)
- run_task
- expect(Champ.where(dossier: dossier).count).to eq(43)
- end
- end
-end
diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb
index f52242650..92f0b0944 100644
--- a/spec/models/dossier_spec.rb
+++ b/spec/models/dossier_spec.rb
@@ -2112,25 +2112,19 @@ describe Dossier, type: :model do
create(:attestation, dossier: dossier)
end
- it "can destroy dossier" do
+ it "can destroy dossier, reset demarche, logg context" do
+ json_message = nil
+ allow(Rails.logger).to receive(:info) { json_message ||= _1 }
+
expect(dossier.destroy).to be_truthy
expect { dossier.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it "can reset demarche" do
- expect { dossier.procedure.reset! }.not_to raise_error
- expect { dossier.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it "call logger with context" do
- json_message = nil
-
- allow(Rails.logger).to receive(:info) { json_message ||= _1 }
- dossier.destroy
expect(JSON.parse(json_message)).to a_hash_including(
{ message: "Dossier destroyed", dossier_id: dossier.id, procedure_id: procedure.id }.stringify_keys
)
+
+ expect { dossier.procedure.reset! }.not_to raise_error
+ expect { dossier.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
diff --git a/spec/models/export_template_spec.rb b/spec/models/export_template_spec.rb
index f0602597a..d90de6744 100644
--- a/spec/models/export_template_spec.rb
+++ b/spec/models/export_template_spec.rb
@@ -1,6 +1,6 @@
describe ExportTemplate do
let(:groupe_instructeur) { create(:groupe_instructeur, procedure:) }
- let(:export_template) { create(:export_template, groupe_instructeur:, content:) }
+ let(:export_template) { create(:export_template, :with_custom_content, groupe_instructeur:, content:) }
let(:procedure) { create(:procedure_with_dossiers, types_de_champ_public:, for_individual:) }
let(:dossier) { procedure.dossiers.first }
let(:for_individual) { false }
@@ -69,6 +69,22 @@ describe ExportTemplate do
end
end
+ describe '#assign_pj_names' do
+ let(:pj_params) do
+ {
+ "tiptap_pj_1" => {
+ "type" => "doc", "content" => [{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "avis-commission-" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] }]
+ }.to_json
+ }
+ end
+ it 'values content from pj params' do
+ export_template.assign_pj_names(pj_params)
+ expect(export_template.content["pjs"]).to eq [
+ { :path => { "content" => [{ "content" => [{ "text" => "avis-commission-", "type" => "text" }, { "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" }, "type" => "mention" }], "type" => "paragraph" }], "type" => "doc" }, :stable_id => "1" }
+ ]
+ end
+ end
+
describe '#tiptap_default_dossier_directory' do
it 'returns tiptap_default_dossier_directory from content' do
expect(export_template.tiptap_default_dossier_directory).to eq({
@@ -174,6 +190,14 @@ describe ExportTemplate do
it 'convert pdf_name' do
expect(export_template.tiptap_convert(procedure.dossiers.first, "pdf_name")).to eq "mon_export_#{dossier.id}"
end
+
+ context 'for date' do
+ let(:export_template) { create(:export_template, :with_date_depot_for_export_pdf, groupe_instructeur:) }
+ let(:dossier) { create(:dossier, :en_construction, procedure:, depose_at: Date.parse("2024/03/30")) }
+ it 'convert date with dash' do
+ expect(export_template.tiptap_convert(dossier, "pdf_name")).to eq "export_#{dossier.id}-2024-03-30"
+ end
+ end
end
describe '#tiptap_convert_pj' do
@@ -297,21 +321,37 @@ describe ExportTemplate do
end
end
- describe 'specific_tags' do
- context 'for entreprise procedure' do
- let(:for_individual) { false }
+ context 'for entreprise procedure' do
+ let(:for_individual) { false }
+ describe 'specific_tags' do
it do
tags = export_template.specific_tags
expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"]
end
end
- context 'for individual procedure' do
- let(:for_individual) { true }
+ describe 'tags_for_pj' do
+ it do
+ tags = export_template.tags_for_pj
+ expect(tags.map { _1[:id] }).to eq ["entreprise_siren", "entreprise_numero_tva_intracommunautaire", "entreprise_siret_siege_social", "entreprise_raison_sociale", "entreprise_adresse", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur", "original-filename"]
+ end
+ end
+ end
+
+ context 'for individual procedure' do
+ let(:for_individual) { true }
+ describe 'specific_tags' do
it do
tags = export_template.specific_tags
expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur"]
end
end
+
+ describe 'tags_for_pj' do
+ it do
+ tags = export_template.tags_for_pj
+ expect(tags.map { _1[:id] }).to eq ["individual_gender", "individual_last_name", "individual_first_name", "dossier_depose_at", "dossier_procedure_libelle", "dossier_service_name", "dossier_number", "dossier_groupe_instructeur", "original-filename"]
+ end
+ end
end
end
diff --git a/spec/models/logic/binary_operator_spec.rb b/spec/models/logic/binary_operator_spec.rb
index e27c3b7bc..b7924ebc7 100644
--- a/spec/models/logic/binary_operator_spec.rb
+++ b/spec/models/logic/binary_operator_spec.rb
@@ -43,6 +43,8 @@ end
describe Logic::GreaterThanEq do
include Logic
+ let(:champ) { create(:champ_integer_number, value: nil) }
+
it 'computes' do
expect(greater_than_eq(constant(0), constant(1)).compute).to be(false)
expect(greater_than_eq(constant(1), constant(1)).compute).to be(true)
diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb
index 9d3ea5924..434d82e44 100644
--- a/spec/models/procedure_revision_spec.rb
+++ b/spec/models/procedure_revision_spec.rb
@@ -347,306 +347,417 @@ describe ProcedureRevision do
end
end
- describe '#compare' do
+ describe '#compare_types_de_champ' do
include Logic
-
- let(:first_tdc) { draft.types_de_champ_public.first }
- let(:second_tdc) { draft.types_de_champ_public.second }
let(:new_draft) { procedure.create_new_revision }
+ subject { procedure.active_revision.compare_types_de_champ(new_draft.reload).map(&:to_h) }
- subject { procedure.active_revision.compare(new_draft.reload).map(&:to_h) }
+ describe 'when tdcs changes' do
+ let(:first_tdc) { draft.types_de_champ_public.first }
+ let(:second_tdc) { draft.types_de_champ_public.second }
- context 'with a procedure with 2 tdcs' do
- let(:procedure) do
- create(:procedure, types_de_champ_public: [
- { type: :integer_number, libelle: 'l1' },
- { type: :text, libelle: 'l2' }
- ])
+ context 'with a procedure with 2 tdcs' do
+ let(:procedure) do
+ create(:procedure, types_de_champ_public: [
+ { type: :integer_number, libelle: 'l1' },
+ { type: :text, libelle: 'l2' }
+ ])
+ end
+
+ context 'when a condition is added' do
+ before do
+ second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
+ second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ attribute: :condition,
+ from: nil,
+ label: "l2",
+ op: :update,
+ private: false,
+ stable_id: second_tdc.stable_id,
+ to: "(l1 == 3)"
+ }
+ ])
+ end
+ end
+
+ context 'when a condition is removed' do
+ before do
+ second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
+ draft.reload
+
+ second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
+ second.update(condition: nil)
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ attribute: :condition,
+ from: "(l1 == 2)",
+ label: "l2",
+ op: :update,
+ private: false,
+ stable_id: second_tdc.stable_id,
+ to: nil
+ }
+ ])
+ end
+ end
+
+ context 'when a condition is changed' do
+ before do
+ second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
+ draft.reload
+
+ second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
+ second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ attribute: :condition,
+ from: "(l1 == 2)",
+ label: "l2",
+ op: :update,
+ private: false,
+ stable_id: second_tdc.stable_id,
+ to: "(l1 == 3)"
+ }
+ ])
+ end
+ end
end
- context 'when a condition is added' do
- before do
- second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
- second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
+ context 'when a type de champ is added' do
+ let(:procedure) { create(:procedure) }
+ let(:new_tdc) do
+ new_draft.add_type_de_champ(
+ type_champ: TypeDeChamp.type_champs.fetch(:text),
+ libelle: "Un champ text"
+ )
end
+ before { new_tdc }
+
it do
is_expected.to eq([
{
- attribute: :condition,
- from: nil,
- label: "l2",
- op: :update,
+ op: :add,
+ label: "Un champ text",
private: false,
- stable_id: second_tdc.stable_id,
- to: "(l1 == 3)"
+ mandatory: false,
+ stable_id: new_tdc.stable_id
}
])
end
end
- context 'when a condition is removed' do
- before do
- second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
- draft.reload
+ context 'when a type de champ is changed' do
+ context 'when libelle, description, and mandatory are changed' do
+ let(:procedure) { create(:procedure, :with_type_de_champ) }
- second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
- second.update(condition: nil)
+ before do
+ updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
+
+ updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ op: :update,
+ attribute: :libelle,
+ label: first_tdc.libelle,
+ private: false,
+ from: first_tdc.libelle,
+ to: "modifier le libelle",
+ stable_id: first_tdc.stable_id
+ },
+ {
+ op: :update,
+ attribute: :description,
+ label: first_tdc.libelle,
+ private: false,
+ from: first_tdc.description,
+ to: "une description",
+ stable_id: first_tdc.stable_id
+ },
+ {
+ op: :update,
+ attribute: :mandatory,
+ label: first_tdc.libelle,
+ private: false,
+ from: false,
+ to: true,
+ stable_id: first_tdc.stable_id
+ }
+ ])
+ end
+ end
+
+ context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) }
+
+ before do
+ updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
+
+ updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
+ end
+ it do
+ is_expected.to eq([
+ {
+ op: :update,
+ attribute: :collapsible_explanation_enabled,
+ label: first_tdc.libelle,
+ private: first_tdc.private?,
+ from: false,
+ to: true,
+ stable_id: first_tdc.stable_id
+ },
+ {
+ op: :update,
+ attribute: :collapsible_explanation_text,
+ label: first_tdc.libelle,
+ private: first_tdc.private?,
+ from: nil,
+ to: 'afficher au clique',
+ stable_id: first_tdc.stable_id
+ }
+ ])
+ end
+ end
+ end
+
+ context 'when a type de champ is moved' do
+ let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) }
+ let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second }
+ let(:new_draft_third_tdc) { new_draft.types_de_champ_public.third }
+
+ before do
+ new_draft_second_tdc
+ new_draft_third_tdc
+ new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2)
end
it do
is_expected.to eq([
{
- attribute: :condition,
- from: "(l1 == 2)",
- label: "l2",
- op: :update,
+ op: :move,
+ label: new_draft_third_tdc.libelle,
private: false,
- stable_id: second_tdc.stable_id,
- to: nil
- }
- ])
- end
- end
-
- context 'when a condition is changed' do
- before do
- second_tdc.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(2)))
- draft.reload
-
- second = new_draft.find_and_ensure_exclusive_use(second_tdc.stable_id)
- second.update(condition: ds_eq(champ_value(first_tdc.stable_id), constant(3)))
- end
-
- it do
- is_expected.to eq([
+ from: 2,
+ to: 1,
+ stable_id: new_draft_third_tdc.stable_id
+ },
{
- attribute: :condition,
- from: "(l1 == 2)",
- label: "l2",
- op: :update,
+ op: :move,
+ label: new_draft_second_tdc.libelle,
private: false,
- stable_id: second_tdc.stable_id,
- to: "(l1 == 3)"
+ from: 1,
+ to: 2,
+ stable_id: new_draft_second_tdc.stable_id
}
])
end
end
- end
- context 'when a type de champ is added' do
- let(:procedure) { create(:procedure) }
- let(:new_tdc) do
- new_draft.add_type_de_champ(
- type_champ: TypeDeChamp.type_champs.fetch(:text),
- libelle: "Un champ text"
- )
- end
-
- before { new_tdc }
-
- it do
- is_expected.to eq([
- {
- op: :add,
- label: "Un champ text",
- private: false,
- mandatory: false,
- stable_id: new_tdc.stable_id
- }
- ])
- end
- end
-
- context 'when a type de champ is changed' do
- context 'when libelle, description, and mandatory are changed' do
+ context 'when a type de champ is removed' do
let(:procedure) { create(:procedure, :with_type_de_champ) }
before do
- updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
-
- updated_tdc.update(libelle: 'modifier le libelle', description: 'une description', mandatory: !updated_tdc.mandatory)
+ new_draft.remove_type_de_champ(first_tdc.stable_id)
end
it do
is_expected.to eq([
{
- op: :update,
- attribute: :libelle,
+ op: :remove,
label: first_tdc.libelle,
private: false,
- from: first_tdc.libelle,
- to: "modifier le libelle",
- stable_id: first_tdc.stable_id
- },
- {
- op: :update,
- attribute: :description,
- label: first_tdc.libelle,
- private: false,
- from: first_tdc.description,
- to: "une description",
- stable_id: first_tdc.stable_id
- },
- {
- op: :update,
- attribute: :mandatory,
- label: first_tdc.libelle,
- private: false,
- from: false,
- to: true,
stable_id: first_tdc.stable_id
}
])
end
end
- context 'when collapsible_explanation_enabled and collapsible_explanation_text are changed' do
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :explication }]) }
+ context 'when a child type de champ is transformed into a drop_down_list' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
before do
- updated_tdc = new_draft.find_and_ensure_exclusive_use(first_tdc.stable_id)
-
- updated_tdc.update(collapsible_explanation_enabled: "1", collapsible_explanation_text: 'afficher au clique')
+ child = new_draft.children_of(new_draft.types_de_champ_public.last).first
+ new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :drop_down_list, drop_down_options: ['one', 'two'])
end
+
it do
is_expected.to eq([
{
op: :update,
- attribute: :collapsible_explanation_enabled,
- label: first_tdc.libelle,
- private: first_tdc.private?,
- from: false,
- to: true,
- stable_id: first_tdc.stable_id
+ attribute: :type_champ,
+ label: "sub type de champ",
+ private: false,
+ from: "text",
+ to: "drop_down_list",
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
},
{
op: :update,
- attribute: :collapsible_explanation_text,
- label: first_tdc.libelle,
- private: first_tdc.private?,
- from: nil,
- to: 'afficher au clique',
- stable_id: first_tdc.stable_id
+ attribute: :drop_down_options,
+ label: "sub type de champ",
+ private: false,
+ from: [],
+ to: ["one", "two"],
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
+ }
+ ])
+ end
+ end
+
+ context 'when a child type de champ is transformed into a map' do
+ let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
+
+ before do
+ child = new_draft.children_of(new_draft.types_de_champ_public.last).first
+ new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :carte, options: { cadastres: true, znieff: true })
+ end
+
+ it do
+ is_expected.to eq([
+ {
+ op: :update,
+ attribute: :type_champ,
+ label: "sub type de champ",
+ private: false,
+ from: "text",
+ to: "carte",
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
+ },
+ {
+ op: :update,
+ attribute: :carte_layers,
+ label: "sub type de champ",
+ private: false,
+ from: [],
+ to: [:cadastres, :znieff],
+ stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
}
])
end
end
end
+ end
- context 'when a type de champ is moved' do
- let(:procedure) { create(:procedure, types_de_champ_public: Array.new(3) { { type: :text } }) }
- let(:new_draft_second_tdc) { new_draft.types_de_champ_public.second }
- let(:new_draft_third_tdc) { new_draft.types_de_champ_public.third }
+ describe 'compare_ineligibilite_rules' do
+ include Logic
+ let(:new_draft) { procedure.create_new_revision }
+ subject { procedure.active_revision.compare_ineligibilite_rules(new_draft.reload) }
- before do
- new_draft_second_tdc
- new_draft_third_tdc
- new_draft.move_type_de_champ(new_draft_second_tdc.stable_id, 2)
+ context 'when ineligibilite_rules changes' do
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :yes_no }] }
+ let(:yes_no_tdc) { new_draft.types_de_champ_public.first }
+
+ context 'when nothing changed' do
+ it { is_expected.to be_empty }
end
- it do
- is_expected.to eq([
- {
- op: :move,
- label: new_draft_third_tdc.libelle,
- private: false,
- from: 2,
- to: 1,
- stable_id: new_draft_third_tdc.stable_id
- },
- {
- op: :move,
- label: new_draft_second_tdc.libelle,
- private: false,
- from: 1,
- to: 2,
- stable_id: new_draft_second_tdc.stable_id
- }
- ])
+ context 'when ineligibilite_rules added' do
+ before do
+ new_draft.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::AddEligibiliteRuleChange)) }
+ end
+
+ context 'when ineligibilite_rules removed' do
+ before do
+ procedure.published_revision.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::RemoveEligibiliteRuleChange)) }
+ end
+
+ context 'when ineligibilite_rules changed' do
+ before do
+ procedure.published_revision.update!(ineligibilite_rules: ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)))
+ new_draft.update!(ineligibilite_rules: ds_and([
+ ds_eq(champ_value(yes_no_tdc.stable_id), constant(true)),
+ empty_operator(empty, empty)
+ ]))
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::UpdateEligibiliteRuleChange)) }
+ end
+
+ context 'when when ineligibilite_enabled changes from false to true' do
+ before do
+ procedure.published_revision.update!(ineligibilite_enabled: false, ineligibilite_message: :required)
+ new_draft.update!(ineligibilite_enabled: true, ineligibilite_message: :required)
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::EligibiliteEnabledChange)) }
+ end
+
+ context 'when ineligibilite_enabled changes from true to false' do
+ before do
+ procedure.published_revision.update!(ineligibilite_enabled: true, ineligibilite_message: :required)
+ new_draft.update!(ineligibilite_enabled: false, ineligibilite_message: :required)
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::EligibiliteDisabledChange)) }
+ end
+
+ context 'when ineligibilite_message changes' do
+ before do
+ procedure.published_revision.update!(ineligibilite_message: :a)
+ new_draft.update!(ineligibilite_message: :b)
+ end
+
+ it { is_expected.to include(an_instance_of(ProcedureRevisionChange::UpdateEligibiliteMessageChange)) }
end
end
+ end
- context 'when a type de champ is removed' do
- let(:procedure) { create(:procedure, :with_type_de_champ) }
-
- before do
- new_draft.remove_type_de_champ(first_tdc.stable_id)
- end
-
- it do
- is_expected.to eq([
- {
- op: :remove,
- label: first_tdc.libelle,
- private: false,
- stable_id: first_tdc.stable_id
- }
- ])
- end
+ describe 'ineligibilite_rules_are_valid?' do
+ include Logic
+ let(:procedure) { create(:procedure) }
+ let(:draft_revision) { procedure.draft_revision }
+ let(:ineligibilite_message) { 'ok' }
+ let(:ineligibilite_enabled) { true }
+ before do
+ procedure.draft_revision.update(ineligibilite_rules:, ineligibilite_message:, ineligibilite_enabled:)
end
- context 'when a child type de champ is transformed into a drop_down_list' do
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
-
- before do
- child = new_draft.children_of(new_draft.types_de_champ_public.last).first
- new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :drop_down_list, drop_down_options: ['one', 'two'])
- end
-
- it do
- is_expected.to eq([
- {
- op: :update,
- attribute: :type_champ,
- label: "sub type de champ",
- private: false,
- from: "text",
- to: "drop_down_list",
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- },
- {
- op: :update,
- attribute: :drop_down_options,
- label: "sub type de champ",
- private: false,
- from: [],
- to: ["one", "two"],
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- }
- ])
+ context 'when ineligibilite_rules are valid' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(true)) }
+ it 'is valid' do
+ expect(draft_revision.validate(:publication)).to be_truthy
+ expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_truthy
end
end
-
- context 'when a child type de champ is transformed into a map' do
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text, libelle: 'sub type de champ' }, { type: :integer_number }] }]) }
-
- before do
- child = new_draft.children_of(new_draft.types_de_champ_public.last).first
- new_draft.find_and_ensure_exclusive_use(child.stable_id).update(type_champ: :carte, options: { cadastres: true, znieff: true })
+ context 'when ineligibilite_rules are invalid on simple champ' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
+ it 'is invalid' do
+ expect(draft_revision.validate(:publication)).to be_falsey
+ expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
end
-
- it do
- is_expected.to eq([
- {
- op: :update,
- attribute: :type_champ,
- label: "sub type de champ",
- private: false,
- from: "text",
- to: "carte",
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- },
- {
- op: :update,
- attribute: :carte_layers,
- label: "sub type de champ",
- private: false,
- from: [],
- to: [:cadastres, :znieff],
- stable_id: new_draft.children_of(new_draft.types_de_champ_public.last).first.stable_id
- }
- ])
+ end
+ context 'when ineligibilite_rules are invalid on repetition champ' do
+ let(:ineligibilite_rules) { ds_eq(constant(true), constant(1)) }
+ let(:procedure) { create(:procedure, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :integer_number }] }] }
+ let(:tdc_number) { draft_revision.types_de_champ_for(scope: :public).find { _1.type_champ == 'integer_number' } }
+ let(:ineligibilite_rules) do
+ ds_eq(champ_value(tdc_number.stable_id), constant(true))
+ end
+ it 'is invalid' do
+ expect(draft_revision.validate(:publication)).to be_falsey
+ expect(draft_revision.validate(:ineligibilite_rules_editor)).to be_falsey
end
end
end
@@ -828,23 +939,22 @@ describe ProcedureRevision do
describe 'conditions_are_valid' do
include Logic
- def first_champ = procedure.draft_revision.types_de_champ_public.first
-
- def second_champ = procedure.draft_revision.types_de_champ_public.second
-
- let(:procedure) do
- create(:procedure).tap do |p|
- p.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'l1')
- p.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'l2')
- end
+ let(:procedure) { create(:procedure, types_de_champ_public:) }
+ let(:types_de_champ_public) do
+ [
+ { type: :integer_number, libelle: 'l1' },
+ { type: :integer_number, libelle: 'l2' }
+ ]
end
+ def first_champ = procedure.draft_revision.types_de_champ_public.first
+ def second_champ = procedure.draft_revision.types_de_champ_public.second
let(:draft_revision) { procedure.draft_revision }
let(:condition) { nil }
subject do
- draft_revision.save
- draft_revision.errors
+ procedure.validate(:publication)
+ procedure.errors
end
context 'when a champ has a valid condition (type)' do
@@ -865,7 +975,7 @@ describe ProcedureRevision do
before { second_champ.update(condition: condition) }
let(:condition) { ds_eq(constant(true), constant(1)) }
- it { expect(subject.first.attribute).to eq(:condition) }
+ it { expect(subject.first.attribute).to eq(:draft_types_de_champ_public) }
end
context 'when a champ has an invalid condition: needed tdc is down in the forms' do
@@ -876,7 +986,7 @@ describe ProcedureRevision do
first_champ.update(condition: need_second_champ)
end
- it { expect(subject.first.attribute).to eq(:condition) }
+ it { expect(subject.first.attribute).to eq(:draft_types_de_champ_public) }
end
context 'with a repetition' do
@@ -904,7 +1014,7 @@ describe ProcedureRevision do
context 'when a champ belongs to a repetition' do
let(:condition) { ds_eq(champ_value(-1), constant(1)) }
- it { expect(subject.first.attribute).to eq(:condition) }
+ it { expect(subject.first.attribute).to eq(:draft_types_de_champ_public) }
end
end
end
@@ -918,8 +1028,8 @@ describe ProcedureRevision do
let(:draft_revision) { procedure.draft_revision }
subject do
- draft_revision.save
- draft_revision.errors
+ procedure.validate(:publication)
+ procedure.errors
end
it 'find error' do
@@ -936,8 +1046,8 @@ describe ProcedureRevision do
let(:draft_revision) { procedure.draft_revision }
subject do
- draft_revision.save
- draft_revision.errors
+ procedure.validate(:publication)
+ procedure.errors
end
context "When no regexp and no example" do
diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb
index 0589985bb..0b8b3f4ed 100644
--- a/spec/models/procedure_spec.rb
+++ b/spec/models/procedure_spec.rb
@@ -211,7 +211,7 @@ describe Procedure do
it { is_expected.to allow_value('text').on(:publication).for(:cadre_juridique) }
context 'with deliberation' do
- let(:procedure) { build(:procedure, cadre_juridique: nil) }
+ let(:procedure) { build(:procedure, cadre_juridique: nil, revisions: [build(:procedure_revision)]) }
it { expect(procedure.valid?(:publication)).to eq(false) }
@@ -352,24 +352,12 @@ describe Procedure do
end
describe 'draft_types_de_champ validations' do
- let(:repetition) { repetition = procedure.draft_revision.types_de_champ_public.find(&:repetition?) }
- let(:text_field) { build(:type_de_champ_text) }
- let(:invalid_repetition_error_message) { 'Le champ « Enfants » doit comporter au moins un champ répétable' }
-
- let(:drop_down) { build(:type_de_champ_drop_down_list, :without_selectable_values, libelle: 'Civilité') }
- let(:invalid_drop_down_error_message) { 'Le champ « Civilité » doit comporter au moins un choix sélectionnable' }
-
- let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :text }, { type: :integer_number }] }]) }
- let(:draft) { procedure.draft_revision }
-
- before do
- draft.revision_types_de_champ.create(type_de_champ: drop_down, position: 100)
-
- repetition.update(libelle: 'Enfants')
- draft.children_of(repetition).destroy_all
- end
+ let(:procedure) { create(:procedure, types_de_champ_public:, types_de_champ_private:) }
context 'on a draft procedure' do
+ let(:types_de_champ_private) { [] }
+ let(:types_de_champ_public) { [{ type: :repetition, libelle: 'Enfants', children: [] }] }
+
it 'doesn’t validate the types de champs' do
procedure.validate
expect(procedure.errors[:draft_types_de_champ_public]).not_to be_present
@@ -377,46 +365,123 @@ describe Procedure do
end
context 'when validating for publication' do
+ let(:types_de_champ_public) do
+ [
+ { type: :repetition, libelle: 'Enfants', children: [] },
+ { type: :drop_down_list, libelle: 'Civilité', options: [] }
+ ]
+ end
+ let(:types_de_champ_private) { [] }
+ 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?)
parent_coordinate = new_draft.revision_types_de_champ.find_by(type_de_champ: repetition)
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
context 'when the champ is private' do
- before do
- repetition.update(private: true)
- drop_down.update(private: true)
+ let(:types_de_champ_private) do
+ [
+ { type: :repetition, libelle: 'Enfants', children: [] },
+ { type: :drop_down_list, libelle: 'Civilité', options: [] }
+ ]
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
+
+ context 'when condition on champ private use public champ' do
+ include Logic
+ let(:types_de_champ_public) { [{ type: :decimal_number, stable_id: 1 }] }
+ let(:types_de_champ_private) { [{ type: :text, condition: ds_eq(champ_value(1), constant(2)), stable_id: 2 }] }
+ it 'validate without context' do
+ procedure.validate
+ expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to be_empty
+ end
+
+ it 'validate allows condition' do
+ procedure.validate(:types_de_champ_private_editor)
+ expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to be_empty
+ end
+ end
+
+ context 'when condition on champ private use public champ having a position higher than the champ private' do
+ include Logic
+
+ let(:types_de_champ_public) do
+ [
+ { type: :decimal_number, stable_id: 1 },
+ { type: :decimal_number, stable_id: 2 }
+ ]
+ end
+
+ let(:types_de_champ_private) do
+ [
+ { type: :text, condition: ds_eq(champ_value(2), constant(2)), stable_id: 3 }
+ ]
+ end
+
+ it 'validate without context' do
+ procedure.validate
+ expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to be_empty
+ end
+
+ it 'validate allows condition' do
+ procedure.validate(:types_de_champ_private_editor)
+ expect(procedure.errors.full_messages_for(:draft_types_de_champ_private)).to be_empty
+ end
+ end
+
+ context 'when condition on champ public use private champ' 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 a une logique conditionnelle invalide" }
+
+ it 'validate without context' do
+ procedure.validate
+ expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).to be_empty
+ end
+
+ it 'validate prevent condition' do
+ procedure.validate(:types_de_champ_public_editor)
+ expect(procedure.errors.full_messages_for(:draft_types_de_champ_public)).to include(error_on_condition)
end
end
end
@@ -1776,6 +1841,37 @@ describe Procedure do
end
end
+ describe "#parsed_latest_zone_labels" do
+ let!(:draft_procedure) { create(:procedure) }
+ let!(:published_procedure) { create(:procedure_with_dossiers, :published, dossiers_count: 2) }
+ let!(:closed_procedure) { create(:procedure, :closed) }
+ let!(:procedure_detail_draft) { ProcedureDetail.new(id: draft_procedure.id, latest_zone_labels: '{ "zone1", "zone2" }') }
+ let!(:procedure_detail_published) { ProcedureDetail.new(id: published_procedure.id, latest_zone_labels: '{ "zone3", "zone4" }') }
+ let!(:procedure_detail_closed) { ProcedureDetail.new(id: closed_procedure.id, latest_zone_labels: '{ "zone5", "zone6" }') }
+ context 'with parsed latest zone labels' do
+ it 'parses the latest zone labels correctly' do
+ expect(procedure_detail_draft.parsed_latest_zone_labels).to eq(["zone1", "zone2"])
+ expect(procedure_detail_published.parsed_latest_zone_labels).to eq(["zone3", "zone4"])
+ expect(procedure_detail_closed.parsed_latest_zone_labels).to eq(["zone5", "zone6"])
+ end
+
+ it 'returns an empty array for invalid JSON' do
+ procedure_detail_draft.latest_zone_labels = '{ invalid json }'
+ expect(procedure_detail_draft.parsed_latest_zone_labels).to eq([])
+ end
+
+ it 'returns an empty array when latest_zone_labels is nil' do
+ procedure_detail_draft.latest_zone_labels = nil
+ expect(procedure_detail_draft.parsed_latest_zone_labels).to eq([])
+ end
+
+ it 'returns an empty array when latest_zone_labels is empty' do
+ procedure_detail_draft.latest_zone_labels = ''
+ expect(procedure_detail_draft.parsed_latest_zone_labels).to eq([])
+ end
+ end
+ end
+
private
def create_dossier_with_pj_of_size(size, procedure)
diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb
index cef564981..341cb33ad 100644
--- a/spec/services/pieces_justificatives_service_spec.rb
+++ b/spec/services/pieces_justificatives_service_spec.rb
@@ -110,7 +110,7 @@ describe PiecesJustificativesService do
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
context 'with export_template' do
- let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) }
+ let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) }
it { expect(subject).to match_array(pj_champ.call(dossier).piece_justificative_file.attachments) }
end
end
@@ -164,6 +164,13 @@ describe PiecesJustificativesService do
end
it { expect(subject).to match_array(dossier.commentaires.first.piece_jointe.attachments) }
+
+ context 'with export_template' do
+ let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ it 'uses specific name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/messagerie")).to be true
+ end
+ end
end
context 'with a pj not safe on a commentaire' do
@@ -180,6 +187,13 @@ describe PiecesJustificativesService do
let!(:witness) { create(:dossier, :with_justificatif) }
it { expect(subject).to match_array(dossier.justificatif_motivation.attachment) }
+
+ context 'with export_template' do
+ let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ it 'uses specific name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/dossier")).to be true
+ end
+ end
end
context 'with a motivation not safe' do
@@ -195,6 +209,16 @@ describe PiecesJustificativesService do
let!(:witness) { create(:dossier, :with_attestation) }
it { expect(subject).to match_array(dossier.attestation.pdf.attachment) }
+ it 'uses default name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template: nil).liste_documents(dossiers).map(&:second)[0].starts_with?("dossier-#{dossier.id}/pieces_justificatives")).to be true
+ end
+
+ context 'with export_template' do
+ let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ it 'uses specific name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/pieces_justificatives")).to be true
+ end
+ end
end
context 'with an etablissement' do
@@ -212,6 +236,17 @@ describe PiecesJustificativesService do
end
it { expect(subject).to match_array([attestation_sociale.attachment, attestation_fiscale.attachment]) }
+
+ it 'uses default name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template: nil).liste_documents(dossiers).map(&:second)[0].starts_with?("dossier-#{dossier.id}/pieces_justificatives")).to be true
+ end
+
+ context 'with export_template' do
+ let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ it 'uses specific name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/pieces_justificatives")).to be true
+ end
+ end
end
end
@@ -323,14 +358,14 @@ describe PiecesJustificativesService do
context 'given an administrateur' do
let(:user_profile) { build(:administrateur) }
- it "doesn't return confidentiel avis.piece_justificative_file" do
+ it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
end
context 'given an instructeur' do
let(:user_profile) { create(:instructeur) }
- it "doesn't return confidentiel avis.piece_justificative_file" do
+ it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
end
@@ -346,7 +381,7 @@ describe PiecesJustificativesService do
let(:experts_procedure) { create(:experts_procedure, expert: user_profile, procedure:) }
let(:avis) { create(:avis, experts_procedure:, dossier: dossier, confidentiel: true) }
let(:user_profile) { create(:expert) }
- it "doesn't return confidentiel avis.piece_justificative_file" do
+ it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
end
@@ -370,21 +405,28 @@ describe PiecesJustificativesService do
context 'given an administrateur' do
let(:user_profile) { build(:administrateur) }
- it "doesn't return confidentiel avis.piece_justificative_file" do
+ it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
end
context 'given an instructeur' do
let(:user_profile) { create(:instructeur) }
- it "doesn't return confidentiel avis.piece_justificative_file" do
+ it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
+
+ context 'with export_template' do
+ let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ it 'uses specific name for dossier directory' do
+ expect(PiecesJustificativesService.new(user_profile:, export_template:).liste_documents(dossiers).map(&:second)[0].starts_with?("DOSSIER-#{dossier.id}/avis")).to be true
+ end
+ end
end
context 'given an expert' do
let(:user_profile) { create(:expert) }
- it "doesn't return confidentiel avis.piece_justificative_file" do
+ it "return confidentiel avis.piece_justificative_file" do
expect(subject.size).to eq(2)
end
end
@@ -397,7 +439,8 @@ describe PiecesJustificativesService do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, children: [{ type: :piece_justificative }] }]) }
let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) }
let(:dossiers) { Dossier.where(id: dossier.id) }
- subject { PiecesJustificativesService.new(user_profile:, export_template: nil).generate_dossiers_export(dossiers) }
+ let(:export_template) { nil }
+ subject { PiecesJustificativesService.new(user_profile:, export_template:).generate_dossiers_export(dossiers) }
it "doesn't update dossier" do
expect { subject }.not_to change { dossier.updated_at }
@@ -409,11 +452,24 @@ describe PiecesJustificativesService do
let!(:not_confidentiel_avis) { create(:avis, :not_confidentiel, dossier: dossier) }
let!(:expert_avis) { create(:avis, :confidentiel, dossier: dossier, expert: user_profile) }
- subject { PiecesJustificativesService.new(user_profile:, export_template: nil).generate_dossiers_export(dossiers) }
+ subject { PiecesJustificativesService.new(user_profile:, export_template:).generate_dossiers_export(dossiers) }
it "includes avis not confidentiel as well as expert's avis" do
expect_any_instance_of(Dossier).to receive(:avis_for_expert).with(user_profile).and_return([])
subject
end
+
+ it 'gives default name to export pdf file' do
+ expect(subject.first.second.starts_with?("dossier-#{dossier.id}/export-#{dossier.id}")).to eq true
+ end
+ end
+
+ context 'with export template' do
+ let(:export_template) { create(:export_template, :with_custom_ddd_prefix, ddd_prefix: "DOSSIER-", groupe_instructeur: procedure.defaut_groupe_instructeur) }
+ subject { PiecesJustificativesService.new(user_profile:, export_template:).generate_dossiers_export(dossiers) }
+
+ it 'gives custom name to export pdf file' do
+ expect(subject.first.second).to eq "DOSSIER-#{dossier.id}/export_#{dossier.id}.pdf"
+ end
end
end
diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb
index b463675be..30488f846 100644
--- a/spec/services/procedure_export_service_spec.rb
+++ b/spec/services/procedure_export_service_spec.rb
@@ -2,7 +2,6 @@ require 'csv'
describe ProcedureExportService do
let(:instructeur) { create(:instructeur) }
- let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) }
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
let(:export_template) { nil }
@@ -18,159 +17,166 @@ describe ProcedureExportService do
let(:avis_sheet) { subject.sheets.third }
let(:repetition_sheet) { subject.sheets.fourth }
- before do
- # change one tdc place to check if the header is ordered
- tdc_first = procedure.active_revision.revision_types_de_champ_public.first
- tdc_last = procedure.active_revision.revision_types_de_champ_public.last
-
- tdc_first.update(position: tdc_last.position + 1)
- procedure.reload
- end
-
describe 'sheets' do
+ let(:procedure) { create(:procedure) }
+
it 'should have a sheet for each record type' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis'])
end
end
describe 'Dossiers sheet' do
- let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
+ context 'with all data for individual' do
+ let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) }
+ let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
- let(:nominal_headers) do
- [
- "ID",
- "Email",
- "FranceConnect ?",
- "Civilité",
- "Nom",
- "Prénom",
- "Dépôt pour un tiers",
- "Nom du mandataire",
- "Prénom du mandataire",
- "Archivé",
- "État du dossier",
- "Dernière mise à jour le",
- "Dernière mise à jour du dossier le",
- "Déposé le",
- "Passé en instruction le",
- "Traité le",
- "Motivation de la décision",
- "Instructeurs",
- "textarea",
- "date",
- "datetime",
- "number",
- "decimal_number",
- "integer_number",
- "checkbox",
- "civilite",
- "email",
- "phone",
- "address",
- "yes_no",
- "simple_drop_down_list",
- "multiple_drop_down_list",
- "linked_drop_down_list",
- "communes",
- "communes (Code INSEE)",
- "communes (Département)",
- "departements",
- "departements (Code)",
- "regions",
- "regions (Code)",
- "pays",
- "pays (Code)",
- "dossier_link",
- "piece_justificative",
- "rna",
- "carte",
- "titre_identite",
- "iban",
- "siret",
- "annuaire_education",
- "cnaf",
- "dgfip",
- "pole_emploi",
- "mesri",
- "text",
- "epci",
- "epci (Code)",
- "epci (Département)",
- "cojo",
- "expression_reguliere",
- "rnf",
- "rnf (Nom)",
- "rnf (Adresse)",
- "rnf (Code INSEE Ville)",
- "rnf (Département)",
- "engagement_juridique"
- ]
- end
+ # before do
+ # # change one tdc place to check if the header is ordered
+ # tdc_first = procedure.active_revision.revision_types_de_champ_public.first
+ # tdc_last = procedure.active_revision.revision_types_de_champ_public.last
- it 'should have headers' do
- expect(dossiers_sheet.headers).to match_array(nominal_headers)
- end
+ # tdc_first.update(position: tdc_last.position + 1)
+ # procedure.reload
+ # end
- it 'should have data' do
- expect(dossiers_sheet.data.size).to eq(1)
- expect(etablissements_sheet.data.size).to eq(1)
+ let(:nominal_headers) do
+ [
+ "ID",
+ "Email",
+ "FranceConnect ?",
+ "Civilité",
+ "Nom",
+ "Prénom",
+ "Dépôt pour un tiers",
+ "Nom du mandataire",
+ "Prénom du mandataire",
+ "Archivé",
+ "État du dossier",
+ "Dernière mise à jour le",
+ "Dernière mise à jour du dossier le",
+ "Déposé le",
+ "Passé en instruction le",
+ "Traité le",
+ "Motivation de la décision",
+ "Instructeurs",
+ "textarea",
+ "date",
+ "datetime",
+ "number",
+ "decimal_number",
+ "integer_number",
+ "checkbox",
+ "civilite",
+ "email",
+ "phone",
+ "address",
+ "simple_drop_down_list",
+ "multiple_drop_down_list",
+ "linked_drop_down_list",
+ "communes",
+ "communes (Code INSEE)",
+ "communes (Département)",
+ "departements",
+ "departements (Code)",
+ "regions",
+ "regions (Code)",
+ "pays",
+ "pays (Code)",
+ "dossier_link",
+ "piece_justificative",
+ "rna",
+ "carte",
+ "titre_identite",
+ "iban",
+ "siret",
+ "annuaire_education",
+ "cnaf",
+ "dgfip",
+ "pole_emploi",
+ "mesri",
+ "text",
+ "epci",
+ "epci (Code)",
+ "epci (Département)",
+ "cojo",
+ "expression_reguliere",
+ "rnf",
+ "rnf (Nom)",
+ "rnf (Adresse)",
+ "rnf (Code INSEE Ville)",
+ "rnf (Département)",
+ "engagement_juridique",
+ "yes_no"
+ ]
+ end
- # SimpleXlsxReader is transforming datetimes in utc... It is only used in test so we just hack around.
- offset = dossier.depose_at.utc_offset
- depose_at = Time.zone.at(dossiers_sheet.data[0][13] - offset.seconds)
- en_instruction_at = Time.zone.at(dossiers_sheet.data[0][14] - offset.seconds)
- expect(dossiers_sheet.data.first.size).to eq(nominal_headers.size)
- expect(depose_at).to eq(dossier.depose_at.round)
- expect(en_instruction_at).to eq(dossier.en_instruction_at.round)
+ it 'should have data' do
+ expect(dossiers_sheet.headers).to match_array(nominal_headers)
+
+ expect(dossiers_sheet.data.size).to eq(1)
+ expect(etablissements_sheet.data.size).to eq(1)
+
+ # SimpleXlsxReader is transforming datetimes in utc... It is only used in test so we just hack around.
+ offset = dossier.depose_at.utc_offset
+ depose_at = Time.zone.at(dossiers_sheet.data[0][13] - offset.seconds)
+ en_instruction_at = Time.zone.at(dossiers_sheet.data[0][14] - offset.seconds)
+ expect(dossiers_sheet.data.first.size).to eq(nominal_headers.size)
+ expect(depose_at).to eq(dossier.depose_at.round)
+ expect(en_instruction_at).to eq(dossier.en_instruction_at.round)
+ end
end
context 'with a birthdate' do
- before { procedure.update(ask_birthday: true) }
+ let(:procedure) { create(:procedure, :published, :for_individual, ask_birthday: true) }
+ let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) }
- let(:birthdate_headers) { nominal_headers.insert(nominal_headers.index('Archivé'), 'Date de naissance') }
-
- it { expect(dossiers_sheet.headers).to match_array(birthdate_headers) }
- it { expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Date de naissance')]).to be_a(Date) }
+ it 'find date de naissance' do
+ expect(dossiers_sheet.headers).to include('Date de naissance')
+ expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Date de naissance')]).to be_a(Date)
+ end
end
context 'with a procedure routee' do
- before { create(:groupe_instructeur, label: '2', procedure: procedure) }
+ let(:procedure) { create(:procedure, :published, :for_individual) }
+ let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) }
+ before { create(:groupe_instructeur, label: '2', procedure:) }
- let(:routee_headers) { nominal_headers.insert(nominal_headers.index('textarea'), 'Groupe instructeur') }
-
- it { expect(dossiers_sheet.headers).to match_array(routee_headers) }
- it { expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut') }
+ it 'find groupe instructeur data' do
+ expect(dossiers_sheet.headers).to include('Groupe instructeur')
+ expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut')
+ end
end
context 'with a dossier having multiple pjs' do
- let!(:dossier_2) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
+ let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :piece_justificative }] }
+ let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) }
+ let!(:dossier_2) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) }
before do
dossier_2.champs_public
.find { _1.is_a? Champs::PieceJustificativeChamp }
.piece_justificative_file
.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
end
- it { expect(dossiers_sheet.data.first.size).to eq(nominal_headers.size) }
+ it { expect(dossiers_sheet.data.first.size).to eq(19) } # default number of header when procedure has only one champ
end
context 'with procedure chorus' do
- let(:procedure) { create(:procedure, :published, :for_individual, :filled_chorus, :with_all_champs) }
- let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, procedure: procedure) }
+ before { expect_any_instance_of(Procedure).to receive(:chorusable?).and_return(true) }
+ let(:procedure) { create(:procedure, :published, :for_individual, :filled_chorus) }
+ let!(:dossier) { create(:dossier, :en_instruction, procedure: procedure) }
it 'includes chorus headers' do
- expected_headers = [
- 'Domaine Fonctionnel',
- 'Référentiel De Programmation',
- 'Centre De Coup'
- ]
-
- expect(dossiers_sheet.headers).to match_array(nominal_headers)
+ expect(dossiers_sheet.headers).to include('Domaine Fonctionnel')
+ expect(dossiers_sheet.headers).to include('Référentiel De Programmation')
+ expect(dossiers_sheet.headers).to include('Centre De Coût')
end
end
end
describe 'Etablissement sheet' do
- let(:procedure) { create(:procedure, :published, :with_all_champs) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :siret, libelle: 'siret' }] }
let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_entreprise, procedure: procedure) }
let(:dossier_etablissement) { etablissements_sheet.data[1] }
@@ -191,54 +197,7 @@ describe ProcedureExportService do
"Traité le",
"Motivation de la décision",
"Instructeurs",
- "textarea",
- "date",
- "datetime",
- "number",
- "decimal_number",
- "integer_number",
- "checkbox",
- "civilite",
- "email",
- "phone",
- "address",
- "yes_no",
- "simple_drop_down_list",
- "multiple_drop_down_list",
- "linked_drop_down_list",
- "communes",
- "communes (Code INSEE)",
- "communes (Département)",
- "departements",
- "departements (Code)",
- "regions",
- "regions (Code)",
- "pays",
- "pays (Code)",
- "dossier_link",
- "piece_justificative",
- "rna",
- "carte",
- "titre_identite",
- "iban",
- "siret",
- "annuaire_education",
- "cnaf",
- "dgfip",
- "pole_emploi",
- "mesri",
- "text",
- "epci",
- "epci (Code)",
- "epci (Département)",
- "cojo",
- "expression_reguliere",
- "rnf",
- "rnf (Nom)",
- "rnf (Adresse)",
- "rnf (Code INSEE Ville)",
- "rnf (Département)",
- "engagement_juridique"
+ 'siret'
]
end
@@ -294,54 +253,7 @@ describe ProcedureExportService do
"Traité le",
"Motivation de la décision",
"Instructeurs",
- "textarea",
- "date",
- "datetime",
- "number",
- "decimal_number",
- "integer_number",
- "checkbox",
- "civilite",
- "email",
- "phone",
- "address",
- "yes_no",
- "simple_drop_down_list",
- "multiple_drop_down_list",
- "linked_drop_down_list",
- "communes",
- "communes (Code INSEE)",
- "communes (Département)",
- "departements",
- "departements (Code)",
- "regions",
- "regions (Code)",
- "pays",
- "pays (Code)",
- "dossier_link",
- "piece_justificative",
- "rna",
- "carte",
- "titre_identite",
- "iban",
- "siret",
- "annuaire_education",
- "cnaf",
- "dgfip",
- "pole_emploi",
- "mesri",
- "text",
- "epci",
- "epci (Code)",
- "epci (Département)",
- "cojo",
- "expression_reguliere",
- "rnf",
- "rnf (Nom)",
- "rnf (Adresse)",
- "rnf (Code INSEE Ville)",
- "rnf (Département)",
- "engagement_juridique"
+ 'siret'
]
end
@@ -352,7 +264,7 @@ describe ProcedureExportService do
end
end
- it 'should have headers' do
+ it 'should have headers and data' do
expect(dossiers_sheet.headers).to match_array(nominal_headers)
expect(etablissements_sheet.headers).to eq([
@@ -391,9 +303,7 @@ describe ProcedureExportService do
"Association date de déclaration",
"Association date de publication"
])
- end
- it 'should have data' do
expect(etablissements_sheet.data.size).to eq(2)
expect(dossier_etablissement[1]).to eq("Dossier")
expect(champ_etablissement[1]).to eq("siret")
@@ -401,10 +311,11 @@ describe ProcedureExportService do
end
describe 'Avis sheet' do
+ let(:procedure) { create(:procedure, :published, :for_individual) }
let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) }
let!(:avis) { create(:avis, :with_answer, dossier: dossier) }
- it 'should have headers' do
+ it 'should have headers and data' do
expect(avis_sheet.headers).to eq([
"Dossier ID",
"Introduction",
@@ -416,14 +327,20 @@ describe ProcedureExportService do
"Instructeur",
"Expert"
])
- end
-
- it 'should have data' do
expect(avis_sheet.data.size).to eq(1)
end
end
describe 'Repetitions sheet' do
+ before do
+ # change one tdc place to check if the header is ordered
+ tdc_first = procedure.active_revision.revision_types_de_champ_public.first
+ tdc_last = procedure.active_revision.revision_types_de_champ_public.last
+
+ tdc_first.update(position: tdc_last.position + 1)
+ procedure.reload
+ end
+
let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public: [{ type: :repetition, children: [{ libelle: 'Nom' }, { libelle: 'Age' }] }]) }
let!(:dossiers) do
[
@@ -509,6 +426,9 @@ describe ProcedureExportService do
end
describe 'to_zip' do
+ let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :piece_justificative, libelle: 'piece_justificative' }] }
+
subject { service.to_zip }
context 'without files' do
it 'does not raises in_batches' do
@@ -529,7 +449,7 @@ describe ProcedureExportService do
context 'with export_template' do
let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) }
let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) }
- let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) }
+ let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) }
before do
allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io")
end
@@ -588,6 +508,9 @@ describe ProcedureExportService do
end
describe 'to_geo_json' do
+ let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) }
+ let(:types_de_champ_public) { [{ type: :carte }] }
+
subject do
service
.to_geo_json
diff --git a/spec/services/procedure_export_service_zip_spec.rb b/spec/services/procedure_export_service_zip_spec.rb
index 0daced35e..e4daaf3ee 100644
--- a/spec/services/procedure_export_service_zip_spec.rb
+++ b/spec/services/procedure_export_service_zip_spec.rb
@@ -2,7 +2,7 @@ describe ProcedureExportService do
let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :piece_justificative, libelle: 'pj' }, { type: :repetition, children: [{ type: :piece_justificative, libelle: 'repet_pj' }] }]) }
let(:dossiers) { create_list(:dossier, 10, procedure: procedure) }
- let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur).tap(&:set_default_values) }
+ let(:export_template) { create(:export_template, groupe_instructeur: procedure.defaut_groupe_instructeur) }
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) }
def pj_champ(d) = d.champs_public.find_by(type: 'Champs::PieceJustificativeChamp')
@@ -39,8 +39,7 @@ describe ProcedureExportService do
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
subject
end
-
- expect(sql_count <= 58).to be_truthy
+ expect(sql_count <= 62).to be_truthy
dossier = dossiers.first
diff --git a/spec/system/accessibilite/wcag_usager_spec.rb b/spec/system/accessibilite/wcag_usager_spec.rb
index d8ad6fd10..16e6b23c2 100644
--- a/spec/system/accessibilite/wcag_usager_spec.rb
+++ b/spec/system/accessibilite/wcag_usager_spec.rb
@@ -1,33 +1,25 @@
describe 'wcag rules for usager', js: true do
- let(:procedure) { create(:procedure, :published, :with_all_champs, :with_service, :for_individual) }
+ let(:procedure) { create(:procedure, :published, :with_service, :for_individual) }
let(:password) { 'a very complicated password' }
let(:litteraire_user) { create(:user, password: password) }
- before do
- procedure.active_revision.types_de_champ_public.find { |tdc| tdc.type_champ == TypeDeChamp.type_champs.fetch(:carte) }.destroy
- end
+ def test_external_links_have_title_says_it_opens_in_a_new_tab
+ links = page.all("a[target=_blank]")
+ expect(links.count).to be_positive
- shared_examples "external links have title says it opens in a new tab" do
- it do
- links = page.all("a[target=_blank]")
- expect(links.count).to be_positive
-
- links.each do |link|
- expect(link[:title]).to include("Nouvel onglet"), "link #{link[:href]} does not have title mentioning it opens in a new tab"
- end
+ links.each do |link|
+ expect(link[:title]).to include("Nouvel onglet"), "link #{link[:href]} does not have title mentioning it opens in a new tab"
end
end
- shared_examples "aria-label do not mix with title attribute" do
- it do
- elements = page.all("[aria-label][title]")
- elements.each do |element|
- expect(element[:title]).to be_blank, "path=#{path}, element title=\"#{element[:title]}\" mixes aria-label and title attributes"
- end
+ def test_aria_label_do_not_mix_with_title_attribute
+ elements = page.all("[aria-label][title]")
+ elements.each do |element|
+ expect(element[:title]).to be_blank, "path=#{path}, element title=\"#{element[:title]}\" mixes aria-label and title attributes"
end
end
- def expect_axe_clean_without_main_navigation
+ def test_expect_axe_clean_without_main_navigation
# On page without main navigation content (like anonymous home page),
# there are either a bug in axe, either dsfr markup is not conform to wcag2a.
# There is no issue on pages having a child navigation.
@@ -35,10 +27,6 @@ describe 'wcag rules for usager', js: true do
expect(page).to be_axe_clean.within("#modal-header__menu").skipping("aria-prohibited-attr")
end
- shared_examples "axe clean without main navigation" do
- it { expect_axe_clean_without_main_navigation }
- end
-
context 'pages without the need to be logged in' do
before do
visit path
@@ -46,16 +34,20 @@ describe 'wcag rules for usager', js: true do
context 'homepage' do
let(:path) { root_path }
- it_behaves_like "axe clean without main navigation"
- it_behaves_like "external links have title says it opens in a new tab"
- it_behaves_like "aria-label do not mix with title attribute"
+ it 'pass wcag tests' do
+ test_external_links_have_title_says_it_opens_in_a_new_tab
+ test_aria_label_do_not_mix_with_title_attribute
+ test_expect_axe_clean_without_main_navigation
+ end
end
context 'sign_up page' do
let(:path) { new_user_registration_path }
- it_behaves_like "axe clean without main navigation"
- it_behaves_like "external links have title says it opens in a new tab"
- it_behaves_like "aria-label do not mix with title attribute"
+ it 'pass wcag tests' do
+ test_external_links_have_title_says_it_opens_in_a_new_tab
+ test_aria_label_do_not_mix_with_title_attribute
+ test_expect_axe_clean_without_main_navigation
+ end
end
scenario 'account confirmation page' do
@@ -66,43 +58,51 @@ describe 'wcag rules for usager', js: true do
perform_enqueued_jobs do
click_button 'Créer un compte'
- expect_axe_clean_without_main_navigation
+ test_expect_axe_clean_without_main_navigation
end
end
context 'sign_up confirmation' do
let(:path) { user_confirmation_path("user[email]" => "some@email.com") }
- it_behaves_like "external links have title says it opens in a new tab"
- it_behaves_like "aria-label do not mix with title attribute"
+ it 'pass wcag tests' do
+ test_external_links_have_title_says_it_opens_in_a_new_tab
+ test_aria_label_do_not_mix_with_title_attribute
+ end
end
context 'sign_in page' do
let(:path) { new_user_session_path }
- it_behaves_like "axe clean without main navigation"
- it_behaves_like "external links have title says it opens in a new tab"
- it_behaves_like "aria-label do not mix with title attribute"
+ it 'pass wcag tests' do
+ test_external_links_have_title_says_it_opens_in_a_new_tab
+ test_aria_label_do_not_mix_with_title_attribute
+ test_expect_axe_clean_without_main_navigation
+ end
end
context 'contact page' do
let(:path) { contact_path }
- it_behaves_like "axe clean without main navigation"
- it_behaves_like "external links have title says it opens in a new tab"
- it_behaves_like "aria-label do not mix with title attribute"
+ it 'pass wcag tests' do
+ test_external_links_have_title_says_it_opens_in_a_new_tab
+ test_aria_label_do_not_mix_with_title_attribute
+ test_expect_axe_clean_without_main_navigation
+ end
end
context 'commencer page' do
let(:path) { commencer_path(path: procedure.path) }
- it_behaves_like "axe clean without main navigation"
- it_behaves_like "external links have title says it opens in a new tab"
- it_behaves_like "aria-label do not mix with title attribute"
+ it 'pass wcag tests' do
+ test_external_links_have_title_says_it_opens_in_a_new_tab
+ test_aria_label_do_not_mix_with_title_attribute
+ test_expect_axe_clean_without_main_navigation
+ end
end
scenario 'commencer page, help dropdown' do
visit commencer_path(path: procedure.reload.path)
page.find("#help-menu_button").click
- expect_axe_clean_without_main_navigation
+ test_expect_axe_clean_without_main_navigation
end
end
@@ -135,7 +135,7 @@ describe 'wcag rules for usager', js: true do
end
context "logged in, depot d'un dossier entreprise" do
- let(:procedure) { create(:procedure, :with_all_champs, :with_service, :published) }
+ let(:procedure) { create(:procedure, :with_service, :published) }
before do
login_as litteraire_user, scope: :user
@@ -163,11 +163,6 @@ describe 'wcag rules for usager', js: true do
dossier
visit dossiers_path
expect(page).to be_axe_clean
- end
-
- scenario 'liste des dossiers et actions sur le dossier' do
- dossier
- visit dossiers_path
page.find("#actions_menu_dossier_#{dossier.id}_button").click
expect(page).to be_axe_clean
end
diff --git a/spec/system/administrateurs/procedure_administrateurs_spec.rb b/spec/system/administrateurs/procedure_administrateurs_spec.rb
index 420c439b3..b2111cd14 100644
--- a/spec/system/administrateurs/procedure_administrateurs_spec.rb
+++ b/spec/system/administrateurs/procedure_administrateurs_spec.rb
@@ -11,26 +11,17 @@ describe 'Administrateurs can manage administrateurs', js: true do
login_as administrateur.user, scope: :user
end
- scenario 'card is clickable' do
+ scenario "card is clickable, and i can send invitation when i'm not a manager" do
+ another_administrateur = create(:administrateur)
visit admin_procedure_path(procedure)
find('#administrateurs').click
expect(page).to have_css("h1", text: "Administrateurs")
- end
- context 'as admin not flagged from manager' do
- let(:manager) { false }
+ fill_in('administrateur_email', with: another_administrateur.email)
- scenario 'the administrator can add another administrator' do
- another_administrateur = create(:administrateur)
- visit admin_procedure_administrateurs_path(procedure)
-
- fill_in('administrateur_email', with: another_administrateur.email)
-
- click_on 'Ajouter comme administrateur'
-
- within('.alert-success') do
- expect(page).to have_content(another_administrateur.email)
- end
+ click_on 'Ajouter comme administrateur'
+ within('.alert-success') do
+ expect(page).to have_content(another_administrateur.email)
end
end
diff --git a/spec/system/administrateurs/procedure_ineligibilite_spec.rb b/spec/system/administrateurs/procedure_ineligibilite_spec.rb
new file mode 100644
index 000000000..e93a6ea5d
--- /dev/null
+++ b/spec/system/administrateurs/procedure_ineligibilite_spec.rb
@@ -0,0 +1,45 @@
+describe 'Administrateurs can edit procedures', js: true do
+ include Logic
+
+ let(:procedure) { create(:procedure, administrateurs: [create(:administrateur)]) }
+ before do
+ login_as procedure.administrateurs.first.user, scope: :user
+ end
+
+ scenario 'setup eligibilite' do
+ # explain no champ compatible
+ visit admin_procedure_path(procedure)
+ expect(page).to have_content("Désactivé")
+
+ # explain which champs are compatible
+ visit edit_admin_procedure_ineligibilite_rules_path(procedure)
+ expect(page).to have_content("Inéligibilité des dossiers")
+ expect(page).to have_content("Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire : ")
+ click_on "Ajouter un champ supportant les conditions d’inéligibilité"
+
+ # setup a compatible champ
+ expect(page).to have_content('Champs du formulaire')
+ click_on 'Ajouter un champ'
+ select "Oui/Non"
+ fill_in "Libellé du champ", with: "Un champ oui non"
+ click_on "Revenir à l'écran de gestion"
+ procedure.reload
+ first_tdc = procedure.draft_revision.types_de_champ.first
+ # back to procedure dashboard, explain you can set it up now
+ expect(page).to have_content('À configurer')
+ visit edit_admin_procedure_ineligibilite_rules_path(procedure)
+
+ # setup rules and stuffs
+ expect(page).to have_content("Inéligibilité des dossiers")
+ fill_in "Message d’inéligibilité", with: "vous n'etes pas eligible"
+ find('label', text: 'Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité').click
+ click_on "Ajouter une règle d’inéligibilité"
+ all('select').first.select 'Un champ oui non'
+ click_on 'Enregistrer'
+
+ # rules are setup
+ wait_until { procedure.reload.draft_revision.ineligibilite_enabled == true }
+ expect(procedure.draft_revision.ineligibilite_message).to eq("vous n'etes pas eligible")
+ expect(procedure.draft_revision.ineligibilite_rules).to eq(ds_eq(champ_value(first_tdc.stable_id), constant(true)))
+ end
+end
diff --git a/spec/system/administrateurs/procedure_publish_spec.rb b/spec/system/administrateurs/procedure_publish_spec.rb
index f1109ec4c..dc1858358 100644
--- a/spec/system/administrateurs/procedure_publish_spec.rb
+++ b/spec/system/administrateurs/procedure_publish_spec.rb
@@ -44,13 +44,10 @@ describe 'Publishing a procedure', js: true do
end
context 'when a procedure isn’t published yet' do
- before do
- visit admin_procedures_path(statut: "brouillons")
- click_on procedure.libelle
- find('#publish-procedure-link').click
- end
-
scenario 'an admin can publish it' do
+ visit admin_procedure_path(procedure)
+ find('#publish-procedure-link').click
+
expect(find_field('procedure_path').value).to eq procedure.path
fill_in 'lien_site_web', with: 'http://some.website'
within('form') { click_on 'Publier' }
@@ -72,10 +69,13 @@ describe 'Publishing a procedure', js: true do
end
scenario 'an error message prevents the publication' do
- expect(page).to have_content('Des problèmes empêchent la publication de la démarche')
- expect(page).to have_content("Le champ « Enfants » doit comporter au moins un champ répétable")
- expect(page).to have_content("L’annotation privée « Civilité » doit comporter au moins un choix sélectionnable")
+ 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")
+
+ visit admin_procedure_publication_path(procedure)
expect(find_field('procedure_path').value).to eq procedure.path
fill_in 'lien_site_web', with: 'http://some.website'
@@ -85,8 +85,9 @@ describe 'Publishing a procedure', js: true do
context 'when the procedure has the same path as another procedure from another admin ' do
scenario 'an error message prevents the publication' do
- expect(find_field('procedure_path').value).to eq procedure.path
+ visit admin_procedure_publication_path(procedure)
fill_in 'procedure_path', with: other_procedure.path
+
expect(page).to have_content 'vous devez la modifier afin de pouvoir publier votre démarche'
fill_in 'lien_site_web', with: 'http://some.website'
@@ -194,7 +195,7 @@ describe 'Publishing a procedure', js: true do
scenario 'an error message prevents the publication' do
visit admin_procedure_path(procedure)
expect(page).to have_content('Des problèmes empêchent la publication des modifications')
- expect(page).to have_link('corriger', href: edit_admin_procedure_mail_template_path(procedure, Mails::InitiatedMail::SLUG))
+ expect(page).to have_link(href: edit_admin_procedure_mail_template_path(procedure, Mails::InitiatedMail::SLUG))
expect(page).to have_button('Publier les modifications', disabled: true)
end
end
diff --git a/spec/system/administrateurs/types_de_champ_spec.rb b/spec/system/administrateurs/types_de_champ_spec.rb
index 8cbba47a7..28023d9b8 100644
--- a/spec/system/administrateurs/types_de_champ_spec.rb
+++ b/spec/system/administrateurs/types_de_champ_spec.rb
@@ -228,9 +228,7 @@ describe 'As an administrateur I can edit types de champ', js: true do
click_on 'Supprimer'
end
end
-
- expect(page).to have_content("Le formulaire contient des erreurs")
- expect(page).to have_content("Le titre de section suivant est invalide, veuillez le corriger :")
+ expect(page).to have_content("devrait être précédé d'un titre de niveau 1")
end
end
diff --git a/spec/system/users/dossier_ineligibilite_spec.rb b/spec/system/users/dossier_ineligibilite_spec.rb
new file mode 100644
index 000000000..5bbb25c75
--- /dev/null
+++ b/spec/system/users/dossier_ineligibilite_spec.rb
@@ -0,0 +1,182 @@
+require 'system/users/dossier_shared_examples.rb'
+
+describe 'Dossier Inéligibilité', js: true do
+ include Logic
+
+ let(:user) { create(:user) }
+ let(:procedure) { create(:procedure, :published, types_de_champ_public:) }
+ let(:dossier) { create(:dossier, procedure:, user:) }
+
+ let(:published_revision) { procedure.published_revision }
+ let(:first_tdc) { published_revision.types_de_champ.first }
+ let(:second_tdc) { published_revision.types_de_champ.second }
+ let(:ineligibilite_message) { 'sry vous pouvez aps soumettre votre dossier' }
+ let(:eligibilite_params) { { ineligibilite_enabled: true, ineligibilite_message: } }
+
+ before do
+ published_revision.update(eligibilite_params.merge(ineligibilite_rules:))
+ login_as user, scope: :user
+ end
+
+ describe 'ineligibilite_rules with a single BinaryOperator' do
+ let(:types_de_champ_public) { [{ type: :yes_no, stable_id: 1 }] }
+ let(:ineligibilite_rules) { ds_eq(champ_value(first_tdc.stable_id), constant(true)) }
+
+ scenario 'can submit, can not submit, reload' do
+ visit brouillon_dossier_path(dossier)
+ # no error while dossier is empty
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # does raise error when dossier is filled with condition that does not match
+ within "#champ-1" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # raise error when dossier is filled with condition that matches
+ within "#champ-1" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: true)
+ expect(page).to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # reload page and see error
+ visit brouillon_dossier_path(dossier)
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: true)
+ expect(page).to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # modal is closable, and we can change our dossier response to be eligible
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ within "#champ-1" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+
+ # it works, yay
+ click_on "Déposer le dossier"
+ wait_until { dossier.reload.en_construction? == true }
+ end
+ end
+
+ describe 'ineligibilite_rules with a Or' do
+ let(:types_de_champ_public) { [{ type: :yes_no, libelle: 'l1' }, { type: :drop_down_list, libelle: 'l2', options: ['Paris', 'Marseille'] }] }
+ let(:ineligibilite_rules) do
+ ds_or([
+ ds_eq(champ_value(first_tdc.stable_id), constant(true)),
+ ds_eq(champ_value(second_tdc.stable_id), constant('Paris'))
+ ])
+ end
+
+ scenario 'can submit, can not submit, can edit, etc...' do
+ visit brouillon_dossier_path(dossier)
+ # no error while dossier is empty
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # first condition matches (so ineligible), cannot submit dossier and error message is clear
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: true)
+ expect(page).to have_content("Vous ne pouvez pas déposer votre dossier")
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ # first condition does not matches, I can conitnue
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+
+ # Now test dossier modification
+ click_on "Déposer le dossier"
+ click_on "Accéder à votre dossier"
+ click_on "Modifier le dossier"
+
+ # first matches, means i'm blocked to send my file.
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: true)
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Non").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: false)
+
+ # second condition matches, means i'm blocked to send my file
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Paris').click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: true)
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ # none of conditions matches, i can submit
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Marseille').click
+ end
+
+ # it works, yay
+ click_on "Déposer les modifications"
+ wait_until { dossier.reload.en_construction? == true }
+ end
+ end
+
+ describe 'ineligibilite_rules with a And and all visible champs' do
+ let(:types_de_champ_public) { [{ type: :yes_no, libelle: 'l1' }, { type: :drop_down_list, libelle: 'l2', options: ['Paris', 'Marseille'] }] }
+ let(:ineligibilite_rules) do
+ ds_and([
+ ds_eq(champ_value(first_tdc.stable_id), constant(true)),
+ ds_eq(champ_value(second_tdc.stable_id), constant('Paris'))
+ ])
+ end
+
+ scenario 'can submit, can not submit, can edit, etc...' do
+ visit brouillon_dossier_path(dossier)
+ # no error while dossier is empty
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # only one condition is matches, can submit dossier
+ within "#champ-#{first_tdc.stable_id}" do
+ find("label", text: "Oui").click
+ end
+ expect(page).to have_selector(:button, text: "Déposer le dossier", disabled: false)
+ expect(page).not_to have_content("Vous ne pouvez pas déposer votre dossier")
+
+ # Now test dossier modification
+ click_on "Déposer le dossier"
+ click_on "Accéder à votre dossier"
+ click_on "Modifier le dossier"
+
+ # second condition matches, means i'm blocked to send my file
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Paris').click
+ end
+ expect(page).to have_selector(:button, text: "Déposer les modifications", disabled: true)
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: true)
+ within("#modal-eligibilite-rules-dialog") { click_on "Fermer" }
+ expect(page).to have_selector("#modal-eligibilite-rules-dialog", visible: false)
+
+ # none of conditions matches, i can submit
+ within "#champ-#{second_tdc.stable_id}" do
+ find("label", text: 'Marseille').click
+ end
+
+ # it works, yay
+ click_on "Déposer les modifications"
+ wait_until { dossier.reload.en_construction? == true }
+ end
+ end
+end
diff --git a/spec/views/administrateurs/experts_procedures/index.html.haml_spec.rb b/spec/views/administrateurs/experts_procedures/index.html.haml_spec.rb
index fcc3aa25c..18b91611f 100644
--- a/spec/views/administrateurs/experts_procedures/index.html.haml_spec.rb
+++ b/spec/views/administrateurs/experts_procedures/index.html.haml_spec.rb
@@ -40,7 +40,7 @@ describe 'administrateurs/experts_procedures/index', type: :view do
context 'when the experts_require_administrateur_invitation is false' do
it 'authorize instructors to invite any expert' do
- expect(rendered).not_to have_content "Affecter des experts à la démarche"
+ expect(rendered).not_to have_content "Entrez les adresses emails des experts que vous souhaitez ajouter à la liste prédéfinie"
end
end
@@ -50,7 +50,7 @@ describe 'administrateurs/experts_procedures/index', type: :view do
subject
end
it 'does not authorize instructors to invite any expert but only those presents in admin list' do
- expect(rendered).to have_content "Affecter des experts à la démarche"
+ expect(rendered).to have_content "Entrez les adresses emails des experts que vous souhaitez ajouter à la liste prédéfinie"
end
end
end
diff --git a/spec/views/shared/dossiers/_edit.html.haml_spec.rb b/spec/views/shared/dossiers/_edit.html.haml_spec.rb
index c242f3ec8..f6ce8f5bf 100644
--- a/spec/views/shared/dossiers/_edit.html.haml_spec.rb
+++ b/spec/views/shared/dossiers/_edit.html.haml_spec.rb
@@ -149,4 +149,17 @@ describe 'shared/dossiers/edit', type: :view do
end
end
end
+
+ context 'when dossier transitions rules are computable and passer_en_construction is false' do
+ let(:types_de_champ_public) { [] }
+ let(:dossier) { create(:dossier, procedure:) }
+
+ before do
+ allow(dossier).to receive(:can_passer_en_construction?).and_return(false)
+ end
+
+ it 'renders broken transitions rules dialog' do
+ expect(subject).to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}")
+ end
+ end
end