Merge branch 'main' into etq-usager-bouton-jdma
This commit is contained in:
commit
97ef01b075
170 changed files with 4076 additions and 1251 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
1
Gemfile
1
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'
|
||||
|
|
118
Gemfile.lock
118
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
|
||||
|
|
|
@ -57,5 +57,14 @@ form.form > .conditionnel {
|
|||
select.alert {
|
||||
border-color: $dark-red;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class Conditions::ConditionsComponent < ApplicationComponent
|
|||
|
||||
def available_targets_for_select
|
||||
@source_tdcs
|
||||
.filter { |tdc| ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(tdc.type_champ) }
|
||||
.filter(&:conditionable?)
|
||||
.map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
|
||||
end
|
||||
|
||||
|
|
34
app/components/conditions/ineligibilite_rules_component.rb
Normal file
34
app/components/conditions/ineligibilite_rules_component.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
class Conditions::IneligibiliteRulesComponent < Conditions::ConditionsComponent
|
||||
include Logic
|
||||
|
||||
def initialize(draft_revision:)
|
||||
@draft_revision = draft_revision
|
||||
@published_revision = draft_revision.procedure.published_revision
|
||||
@condition = draft_revision.ineligibilite_rules
|
||||
@source_tdcs = draft_revision.types_de_champ_for(scope: :public)
|
||||
end
|
||||
|
||||
def pending_changes?
|
||||
return false if !@published_revision
|
||||
|
||||
!@published_revision.compare_ineligibilite_rules(@draft_revision).empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def input_prefix
|
||||
'procedure_revision[condition_form]'
|
||||
end
|
||||
|
||||
def input_id_for(name, row_index)
|
||||
"#{@draft_revision.id}-#{name}-#{row_index}"
|
||||
end
|
||||
|
||||
def delete_condition_path(row_index)
|
||||
delete_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id, row_index:)
|
||||
end
|
||||
|
||||
def add_condition_path
|
||||
add_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
fr:
|
||||
display_if: Bloquer si
|
||||
select: Sélectionner
|
||||
add_condition: Ajouter une règle d’inéligibilité
|
||||
remove_a_row: Supprimer une règle
|
|
@ -0,0 +1,49 @@
|
|||
%div{ id: dom_id(@draft_revision, :ineligibilite_rules) }
|
||||
= render Procedure::PendingRepublishComponent.new(procedure: @draft_revision.procedure, render_if: pending_changes?)
|
||||
= render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs)
|
||||
.fr-fieldset
|
||||
= form_for(@draft_revision, url: change_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), html: { id: 'ineligibilite_form', class: 'width-100' }) do |f|
|
||||
.fr-fieldset__element
|
||||
.fr-toggle.fr-toggle--label-left
|
||||
= f.check_box :ineligibilite_enabled, class: 'fr-toggle__input', data: @opt
|
||||
= f.label :ineligibilite_enabled, "Bloquer le dépôt des dossiers répondant à des conditions d’inéligibilité", data: { 'fr-checked-label': "Activé", 'fr-unchecked-label': "Désactivé" }, class: 'fr-toggle__label'
|
||||
|
||||
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :ineligibilite_message, input_type: :text_area, opts: {rows: 5})
|
||||
|
||||
.fr-mx-1w.fr-label.fr-py-0.fr-mb-1w.fr-mt-2w
|
||||
Conditions d’inéligibilité
|
||||
%span.fr-hint-text Vous pouvez utiliser une ou plusieurs condtions pour bloquer le dépot.
|
||||
.fr-fieldset__element
|
||||
= form_tag admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), method: :patch, data: { turbo: true, controller: 'autosave' }, class: 'form width-100' do
|
||||
.conditionnel.width-100
|
||||
%table.condition-table
|
||||
- if rows.size > 0
|
||||
%thead
|
||||
%tr
|
||||
%th.fr-pt-0.far-left
|
||||
%th.fr-pt-0.target Champ Cible
|
||||
%th.fr-pt-0.operator Opérateur
|
||||
%th.fr-pt-0.value Valeur
|
||||
%th.fr-pt-0.delete-column
|
||||
%tbody
|
||||
- rows.each.with_index do |(targeted_champ, operator_name, value), row_index|
|
||||
%tr
|
||||
%td.far-left= far_left_tag(row_index)
|
||||
%td.target= left_operand_tag(targeted_champ, row_index)
|
||||
%td.operator= operator_tag(operator_name, targeted_champ, row_index)
|
||||
%td.value= right_operand_tag(targeted_champ, value, row_index, operator_name)
|
||||
%td.delete-column= delete_condition_tag(row_index)
|
||||
%tfoot
|
||||
%tr
|
||||
%td.text-right{ colspan: 5 }= add_condition_tag
|
||||
|
||||
.padded-fixed-footer
|
||||
.fixed-footer
|
||||
.fr-container
|
||||
.fr-grid-row.fr-col-offset-md-2.fr-col-md-8
|
||||
.fr-col-12
|
||||
%ul.fr-btns-group.fr-btns-group--inline-md
|
||||
%li
|
||||
= link_to "Annuler et revenir à l'écran de gestion", admin_procedure_path(id: @draft_revision.procedure), class: 'fr-btn fr-btn--secondary', data: { confirm: 'Si vous avez fait des modifications elles ne seront pas sauvegardées.'}
|
||||
%li
|
||||
= button_tag "Enregistrer", class: "fr-btn", form: 'ineligibilite_form'
|
|
@ -1,4 +1,6 @@
|
|||
class Dossiers::EditFooterComponent < ApplicationComponent
|
||||
delegate :can_passer_en_construction?, to: :@dossier
|
||||
|
||||
def initialize(dossier:, annotation:)
|
||||
@dossier = dossier
|
||||
@annotation = annotation
|
||||
|
@ -14,20 +16,29 @@ class Dossiers::EditFooterComponent < ApplicationComponent
|
|||
@annotation.present?
|
||||
end
|
||||
|
||||
def disabled_submit_buttons_options
|
||||
{
|
||||
class: 'fr-text--sm fr-mb-0 fr-mr-2w',
|
||||
data: { 'fr-opened': "true" },
|
||||
aria: { controls: 'modal-eligibilite-rules-dialog' }
|
||||
}
|
||||
end
|
||||
|
||||
def submit_draft_button_options
|
||||
{
|
||||
class: 'fr-btn fr-btn--sm',
|
||||
disabled: !owner?,
|
||||
disabled: !owner? || !can_passer_en_construction?,
|
||||
method: :post,
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' }
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server }
|
||||
}
|
||||
end
|
||||
|
||||
def submit_en_construction_button_options
|
||||
{
|
||||
class: 'fr-btn fr-btn--sm',
|
||||
disabled: !can_passer_en_construction?,
|
||||
method: :post,
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' },
|
||||
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit', turbo_force: :server },
|
||||
form: { id: "form-submit-en-construction" }
|
||||
}
|
||||
end
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
en:
|
||||
submit: Submit the file
|
||||
submit_changes: Submit file changes
|
||||
submit_disabled: File submission disabled
|
||||
submitting: Submitting…
|
||||
invite_notice: You are invited to make amendments to this file but <strong>only the owner themselves can submit it</strong>.
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
fr:
|
||||
submit: Déposer le dossier
|
||||
submit_changes: Déposer les modifications
|
||||
submit_disabled: Pourquoi je ne peux pas déposer mon dossier ?
|
||||
submitting: Envoi en cours…
|
||||
invite_notice: En tant qu’invité, vous pouvez remplir ce formulaire – mais <strong>le titulaire du dossier doit le déposer lui-même</strong>.
|
||||
|
|
|
@ -3,8 +3,13 @@
|
|||
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
|
||||
|
||||
- if !annotation? && @dossier.can_transition_to_en_construction?
|
||||
- if !can_passer_en_construction?
|
||||
= link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
|
||||
= button_to t('.submit'), brouillon_dossier_url(@dossier), submit_draft_button_options
|
||||
- elsif @dossier.forked_with_changes?
|
||||
|
||||
- if @dossier.forked_with_changes?
|
||||
- if !can_passer_en_construction?
|
||||
= link_to t('.submit_disabled'), "#", disabled_submit_buttons_options
|
||||
= button_to t('.submit_changes'), modifier_dossier_url(@dossier.editing_fork_origin), submit_en_construction_button_options
|
||||
|
||||
|
||||
|
|
|
@ -3,17 +3,15 @@
|
|||
class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
|
||||
ErrorDescriptor = Data.define(:anchor, :label, :error_message)
|
||||
|
||||
def initialize(dossier:, errors:)
|
||||
def initialize(dossier:)
|
||||
@dossier = dossier
|
||||
@errors = errors
|
||||
end
|
||||
|
||||
def dedup_and_partitioned_errors
|
||||
formated_errors = @errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
|
||||
@dossier.errors.to_enum # ActiveModel::Errors.to_a is an alias to full_messages, we don't want that
|
||||
.to_a # but enum.to_a gives back an array
|
||||
.uniq { |error| [error.inner_error.base] } # dedup cumulated errors from dossier.champs, dossier.champs_public, dossier.champs_private which run the validator one time per association
|
||||
.map { |error| to_error_descriptor(error) }
|
||||
yield(Array(formated_errors[0..2]), Array(formated_errors[3..]))
|
||||
end
|
||||
|
||||
def to_error_descriptor(error)
|
||||
|
@ -27,6 +25,6 @@ class Dossiers::ErrorsFullMessagesComponent < ApplicationComponent
|
|||
end
|
||||
|
||||
def render?
|
||||
!@errors.empty?
|
||||
!@dossier.errors.empty?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,4 +5,3 @@ en:
|
|||
Your file has 1 error. <a href="%{url}">Fix-it</a> to continue :
|
||||
other: |
|
||||
Your file has %{count} errors. <a href="%{url}">Fix-them</a> to continue :
|
||||
see_more: Show all errors
|
||||
|
|
|
@ -5,4 +5,3 @@ fr:
|
|||
Votre dossier contient 1 champ en erreur. <a href="%{url}">Corrigez-la</a> pour poursuivre :
|
||||
other: |
|
||||
Votre dossier contient %{count} champs en erreurs. <a href="%{url}">Corrigez-les</a> pour poursuivre :
|
||||
see_more: Afficher toutes les erreurs
|
||||
|
|
|
@ -1,15 +1,4 @@
|
|||
.fr-alert.fr-alert--error.fr-mb-3w{ role: "alertdialog" }
|
||||
- dedup_and_partitioned_errors do |head, tail|
|
||||
%p#sumup-errors= t('.sumup_html', count: head.size + tail.size, url: head.first.anchor)
|
||||
%ul.fr-mb-0#head-errors
|
||||
- head.each do |error_descriptor|
|
||||
%li
|
||||
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
|
||||
= error_descriptor.error_message
|
||||
- if tail.size > 0
|
||||
%button{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline" }= t('.see_more')
|
||||
%ul#tail-errors.fr-collapse.fr-mt-0
|
||||
- tail.each do |error_descriptor|
|
||||
%li
|
||||
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
|
||||
= "(#{error_descriptor.error_message})"
|
||||
- if dedup_and_partitioned_errors.size > 0
|
||||
%p#sumup-errors= t('.sumup_html', count: dedup_and_partitioned_errors.size, url: dedup_and_partitioned_errors.first.anchor)
|
||||
= render ExpandableErrorList.new(errors: dedup_and_partitioned_errors)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
class Dossiers::InvalidIneligibiliteRulesComponent < ApplicationComponent
|
||||
delegate :can_passer_en_construction?, to: :@dossier
|
||||
|
||||
def initialize(dossier:)
|
||||
@dossier = dossier
|
||||
@revision = dossier.revision
|
||||
end
|
||||
|
||||
def render?
|
||||
!can_passer_en_construction?
|
||||
end
|
||||
|
||||
def error_message
|
||||
@dossier.revision.ineligibilite_message
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
fr:
|
||||
modal:
|
||||
title: "Your file does not match submission criteria"
|
||||
close: "Close"
|
||||
close_alt: "Close this modal"
|
||||
body: "The procedure « %{procedure_libelle} » have submission criteria, unfortunately your file does not match them. You can not submit your file"
|
|
@ -0,0 +1,5 @@
|
|||
fr:
|
||||
modal:
|
||||
title: "Vous ne pouvez pas déposer votre dossier"
|
||||
close: "Fermer"
|
||||
close_alt: "Fermer la fenêtre modale"
|
|
@ -0,0 +1,16 @@
|
|||
%div{ id: dom_id(@dossier, :ineligibilite_rules_broken), data: { controller: 'ineligibilite-rules-match', turbo_force: :server } }
|
||||
%button.fr-sr-only{ aria: {controls: 'modal-eligibilite-rules-dialog' }, data: {'fr-opened': "false" } }
|
||||
show modal
|
||||
|
||||
%dialog.fr-modal{ "aria-labelledby" => "fr-modal-title-modal-1", role: "dialog", id: 'modal-eligibilite-rules-dialog', data: { 'ineligibilite-rules-match-target' => 'dialog' } }
|
||||
.fr-container.fr-container--fluid.fr-container-md
|
||||
.fr-grid-row.fr-grid-row--center
|
||||
.fr-col-12.fr-col-md-8.fr-col-lg-6
|
||||
.fr-modal__body
|
||||
.fr-modal__header
|
||||
%button.fr-btn--close.fr-btn{ aria: { controls: 'modal-eligibilite-rules-dialog' }, title: t('.modal.close_alt') }= t('.modal.close')
|
||||
.fr-modal__content
|
||||
%h1#fr-modal-title-modal-1.fr-modal__title
|
||||
%span.fr-icon-arrow-right-line.fr-icon--lg>
|
||||
= t('.modal.title')
|
||||
%p= error_message
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
9
app/components/expandable_error_list.rb
Normal file
9
app/components/expandable_error_list.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class ExpandableErrorList < ApplicationComponent
|
||||
def initialize(errors:)
|
||||
@errors = errors
|
||||
end
|
||||
|
||||
def splitted_errors
|
||||
yield(Array(@errors[0..2]), Array(@errors[3..]))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
en:
|
||||
see_more: Show all errors
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
fr:
|
||||
see_more: Afficher toutes les erreurs
|
|
@ -0,0 +1,14 @@
|
|||
- splitted_errors do |head, tail|
|
||||
%ul#head-errors.fr-mb-0
|
||||
- head.each do |error_descriptor|
|
||||
%li
|
||||
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
|
||||
= error_descriptor.error_message
|
||||
|
||||
- if tail.size > 0
|
||||
%button.fr-mt-0.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "" }= t('see_more')
|
||||
%ul#tail-errors.fr-collapse.fr-mt-0
|
||||
- tail.each do |error_descriptor|
|
||||
%li
|
||||
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
|
||||
= error_descriptor.error_message
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
class Procedure::Card::IneligibiliteDossierComponent < ApplicationComponent
|
||||
def initialize(procedure:)
|
||||
@procedure = procedure
|
||||
end
|
||||
|
||||
def ready?
|
||||
@procedure.draft_revision
|
||||
.conditionable_types_de_champ
|
||||
.present? && @procedure.draft_revision.ineligibilite_enabled
|
||||
end
|
||||
|
||||
def error?
|
||||
!@procedure.draft_revision.validate(:ineligibilite_rules_editor)
|
||||
end
|
||||
|
||||
def completed?
|
||||
@procedure.draft_revision.ineligibilite_enabled
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fr:
|
||||
title: Inéligibilité des dossiers
|
||||
state:
|
||||
pending: Désactivé
|
||||
ready: À configurer
|
||||
completed: Activé
|
||||
subtitle: Gérez vos conditions d’inéligibilité en fonction des champs du formulaire
|
|
@ -0,0 +1,13 @@
|
|||
.fr-col-6.fr-col-md-4.fr-col-lg-3
|
||||
= link_to edit_admin_procedure_ineligibilite_rules_path(@procedure), class: 'fr-tile fr-enlarge-link' do
|
||||
.fr-tile__body.flex.column.align-center.justify-between
|
||||
- if !ready?
|
||||
%p.fr-badge.fr-badge= t('.state.pending')
|
||||
- elsif error?
|
||||
%p.fr-badge.fr-badge--error À modifier
|
||||
- else
|
||||
%p.fr-badge.fr-badge--success= t('.state.completed')
|
||||
%div
|
||||
%h3.fr-h6.fr-mt-10v= t('.title')
|
||||
%p.fr-tile-subtitle= t('.subtitle')
|
||||
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')
|
|
@ -1,9 +0,0 @@
|
|||
class Procedure::Card::ModificationsComponent < ApplicationComponent
|
||||
def initialize(procedure:)
|
||||
@procedure = procedure
|
||||
end
|
||||
|
||||
def render?
|
||||
@procedure.revised?
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
fr:
|
||||
title:
|
||||
one: Modification du formulaire
|
||||
other: Modifications du formulaire
|
|
@ -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
|
60
app/components/procedure/errors_summary.rb
Normal file
60
app/components/procedure/errors_summary.rb
Normal file
|
@ -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
|
|
@ -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:)
|
10
app/components/procedure/pending_republish_component.rb
Normal file
10
app/components/procedure/pending_republish_component.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class Procedure::PendingRepublishComponent < ApplicationComponent
|
||||
def initialize(procedure:, render_if:)
|
||||
@procedure = procedure
|
||||
@render_if = render_if
|
||||
end
|
||||
|
||||
def render?
|
||||
@render_if
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
fr:
|
||||
pending_republish_html: |
|
||||
Ces modifications ne seront appliquées qu'à la prochaine publication. Vous pouvez vérifier puis publier les modifications sur l'écran de <a href="%{href}">gestion de la démarche</a>
|
|
@ -0,0 +1,3 @@
|
|||
= render Dsfr::AlertComponent.new(state: :warning) do |c|
|
||||
- c.with_body do
|
||||
= t('.pending_republish_html', href: admin_procedure_path(@procedure.id))
|
|
@ -1,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
|
|
@ -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'})"
|
|
@ -1,9 +1,13 @@
|
|||
class Procedure::RevisionChangesComponent < ApplicationComponent
|
||||
def initialize(changes:, previous_revision:)
|
||||
@changes = changes
|
||||
def initialize(new_revision:, previous_revision:)
|
||||
@previous_revision = previous_revision
|
||||
@public_move_changes, @private_move_changes = changes.filter { _1.op == :move }.partition { !_1.private? }
|
||||
@delete_champ_warning = !total_dossiers.zero? && !@changes.all?(&:can_rebase?)
|
||||
@new_revision = new_revision
|
||||
|
||||
@tdc_changes = previous_revision.compare_types_de_champ(new_revision)
|
||||
@public_move_changes, @private_move_changes = @tdc_changes.filter { _1.op == :move }.partition { !_1.private? }
|
||||
@delete_champ_warning = !total_dossiers.zero? && !@tdc_changes.all?(&:can_rebase?)
|
||||
|
||||
@ineligibilite_rules_changes = previous_revision.compare_ineligibilite_rules(new_revision)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -80,3 +80,10 @@ fr:
|
|||
update_expression_reguliere_exemple_text: L’exemple d’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouvel exemple est « %{to} ».
|
||||
remove_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été supprimé.
|
||||
update_expression_reguliere_error_message: Le message d’erreur de l’expression régulière de l’annotation privée « %{label} » a été modifiée. Le nouveau message est « %{to} ».
|
||||
ineligibilite_rules:
|
||||
add: La condition d’inéligibilité « %{new_condition} » a été ajoutée.
|
||||
remove: La condition d’inéligibilité « %{previous_condition} » a été supprimée
|
||||
update: La conditon d’inéligibilité « %{previous_condition} » a été changée pour « %{new_condition} »
|
||||
enabled: "L’inéligibilité des dossiers a été activée"
|
||||
disabled: "L’inéligibilité des dossiers a été désactivée"
|
||||
message_updated: "Le message d’inéligibilité a été changé pour « %{ineligibilite_message} »"
|
|
@ -2,7 +2,7 @@
|
|||
- list.with_empty do
|
||||
= t('.no_changes')
|
||||
|
||||
- @changes.each do |change|
|
||||
- @tdc_changes.each do |change|
|
||||
- prefix = change.private? ? 'private' : 'public'
|
||||
- case change.op
|
||||
- when :add
|
||||
|
@ -176,3 +176,7 @@
|
|||
- list.with_item do
|
||||
.fr-alert.fr-alert--warning.fr-mt-1v
|
||||
= t(".invalid_routing_rules_alert")
|
||||
|
||||
- @ineligibilite_rules_changes.each do |change|
|
||||
- list.with_item do
|
||||
= t(".ineligibilite_rules.#{change.op}", **change.i18n_params)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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
|
|
@ -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 :'
|
|
@ -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)
|
|
@ -31,7 +31,7 @@ class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent
|
|||
end
|
||||
|
||||
def errors?
|
||||
!errors.empty?
|
||||
errors.present?
|
||||
end
|
||||
|
||||
def to_html_list(messages)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
5
app/controllers/email_checker_controller.rb
Normal file
5
app/controllers/email_checker_controller.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class EmailCheckerController < ApplicationController
|
||||
def show
|
||||
render json: EmailChecker.new.check(email: params[:email])
|
||||
end
|
||||
end
|
|
@ -231,9 +231,9 @@ module Users
|
|||
|
||||
def submit_brouillon
|
||||
@dossier = dossier_with_champs(pj_template: false)
|
||||
@errors = submit_dossier_and_compute_errors
|
||||
submit_dossier_and_compute_errors
|
||||
|
||||
if @errors.blank?
|
||||
if @dossier.errors.blank? && @dossier.can_passer_en_construction?
|
||||
@dossier.passer_en_construction!
|
||||
@dossier.process_declarative!
|
||||
@dossier.process_sva_svr!
|
||||
|
@ -278,9 +278,9 @@ module Users
|
|||
editing_fork_origin.resolve_pending_correction
|
||||
end
|
||||
|
||||
@errors = submit_dossier_and_compute_errors
|
||||
submit_dossier_and_compute_errors
|
||||
|
||||
if @errors.blank?
|
||||
if @dossier.errors.blank? && @dossier.can_passer_en_construction?
|
||||
editing_fork_origin.merge_fork(@dossier)
|
||||
editing_fork_origin.submit_en_construction!
|
||||
|
||||
|
@ -288,7 +288,6 @@ module Users
|
|||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@dossier = editing_fork_origin
|
||||
render :modifier
|
||||
end
|
||||
|
||||
|
@ -303,10 +302,10 @@ module Users
|
|||
def update
|
||||
@dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier
|
||||
@dossier = dossier_with_champs(pj_template: false)
|
||||
@errors = update_dossier_and_compute_errors
|
||||
|
||||
@dossier.index_search_terms_later if @errors.empty?
|
||||
|
||||
@can_passer_en_construction_was = @dossier.can_passer_en_construction?
|
||||
update_dossier_and_compute_errors
|
||||
@dossier.index_search_terms_later if @dossier.errors.empty?
|
||||
@can_passer_en_construction_is = @dossier.can_passer_en_construction?
|
||||
respond_to do |format|
|
||||
format.turbo_stream do
|
||||
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?))
|
||||
|
@ -567,21 +566,14 @@ module Users
|
|||
|
||||
def submit_dossier_and_compute_errors
|
||||
@dossier.validate(:champs_public_value)
|
||||
|
||||
errors = @dossier.errors
|
||||
@dossier.check_mandatory_and_visible_champs.each do |error_on_champ|
|
||||
errors.import(error_on_champ)
|
||||
end
|
||||
@dossier.check_mandatory_and_visible_champs
|
||||
|
||||
if @dossier.editing_fork_origin&.pending_correction?
|
||||
@dossier.editing_fork_origin.validate(:champs_public_value)
|
||||
@dossier.editing_fork_origin.errors.where(:pending_correction).each do |error|
|
||||
errors.import(error)
|
||||
@dossier.errors.import(error)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
errors
|
||||
end
|
||||
|
||||
def ensure_ownership!
|
||||
|
|
|
@ -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!
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
|
|
24
app/graphql/mutations/dossier_supprimer_message.rb
Normal file
24
app/graphql/mutations/dossier_supprimer_message.rb
Normal file
|
@ -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
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
27
app/helpers/gallery_helper.rb
Normal file
27
app/helpers/gallery_helper.rb
Normal file
|
@ -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
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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?
|
||||
|
|
652
app/lib/email_checker.rb
Normal file
652
app/lib/email_checker.rb
Normal file
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -21,6 +21,10 @@ module ChampConditionalConcern
|
|||
end
|
||||
end
|
||||
|
||||
def reset_visible # recompute after a dossier update
|
||||
remove_instance_variable :@visible if instance_variable_defined? :@visible
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def champs_for_condition
|
||||
|
|
|
@ -22,7 +22,7 @@ module DossierRebaseConcern
|
|||
end
|
||||
|
||||
def pending_changes
|
||||
procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
|
||||
procedure.published_revision.present? ? revision.compare_types_de_champ(procedure.published_revision) : []
|
||||
end
|
||||
|
||||
def can_rebase_mandatory_change?(stable_id)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") { '' }],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
[]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
class ProcedureRevisionChange
|
||||
attr_reader :type_de_champ
|
||||
def initialize(type_de_champ)
|
||||
@type_de_champ = type_de_champ
|
||||
class TypeDeChange
|
||||
attr_reader :type_de_champ
|
||||
def initialize(type_de_champ)
|
||||
@type_de_champ = type_de_champ
|
||||
end
|
||||
|
||||
def label = @type_de_champ.libelle
|
||||
def stable_id = @type_de_champ.stable_id
|
||||
def private? = @type_de_champ.private?
|
||||
def child? = @type_de_champ.child?
|
||||
|
||||
def to_h = { op:, stable_id:, label:, private: private? }
|
||||
end
|
||||
|
||||
def label = @type_de_champ.libelle
|
||||
def stable_id = @type_de_champ.stable_id
|
||||
def private? = @type_de_champ.private?
|
||||
def child? = @type_de_champ.child?
|
||||
|
||||
def to_h = { op:, stable_id:, label:, private: private? }
|
||||
|
||||
class AddChamp < ProcedureRevisionChange
|
||||
class AddChamp < TypeDeChange
|
||||
def initialize(type_de_champ)
|
||||
super(type_de_champ)
|
||||
end
|
||||
|
@ -23,7 +25,7 @@ class ProcedureRevisionChange
|
|||
def to_h = super.merge(mandatory: mandatory?)
|
||||
end
|
||||
|
||||
class RemoveChamp < ProcedureRevisionChange
|
||||
class RemoveChamp < TypeDeChange
|
||||
def initialize(type_de_champ)
|
||||
super(type_de_champ)
|
||||
end
|
||||
|
@ -32,7 +34,7 @@ class ProcedureRevisionChange
|
|||
def can_rebase?(dossier = nil) = true
|
||||
end
|
||||
|
||||
class MoveChamp < ProcedureRevisionChange
|
||||
class MoveChamp < TypeDeChange
|
||||
attr_reader :from, :to
|
||||
|
||||
def initialize(type_de_champ, from, to)
|
||||
|
@ -46,7 +48,7 @@ class ProcedureRevisionChange
|
|||
def to_h = super.merge(from:, to:)
|
||||
end
|
||||
|
||||
class UpdateChamp < ProcedureRevisionChange
|
||||
class UpdateChamp < TypeDeChange
|
||||
attr_reader :attribute, :from, :to
|
||||
|
||||
def initialize(type_de_champ, attribute, from, to)
|
||||
|
@ -75,4 +77,48 @@ class ProcedureRevisionChange
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class EligibiliteRulesChange
|
||||
attr_reader :previous_revision, :new_revision
|
||||
def initialize(previous_revision, new_revision)
|
||||
@previous_revision = previous_revision
|
||||
@new_revision = new_revision
|
||||
@previous_ineligibilite_rules = @previous_revision.ineligibilite_rules
|
||||
@new_ineligibilite_rules = @new_revision.ineligibilite_rules
|
||||
end
|
||||
|
||||
def i18n_params
|
||||
{
|
||||
previous_condition: @previous_ineligibilite_rules&.to_s(previous_revision.types_de_champ.filter { @previous_ineligibilite_rules.sources.include? _1.stable_id }),
|
||||
new_condition: @new_ineligibilite_rules&.to_s(new_revision.types_de_champ.filter { @new_ineligibilite_rules.sources.include? _1.stable_id })
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class AddEligibiliteRuleChange < EligibiliteRulesChange
|
||||
def op = :add
|
||||
end
|
||||
|
||||
class RemoveEligibiliteRuleChange < EligibiliteRulesChange
|
||||
def op = :remove
|
||||
end
|
||||
|
||||
class UpdateEligibiliteRuleChange < EligibiliteRulesChange
|
||||
def op = :update
|
||||
end
|
||||
|
||||
class EligibiliteEnabledChange < EligibiliteRulesChange
|
||||
def op = :enabled
|
||||
def i18n_params = {}
|
||||
end
|
||||
|
||||
class EligibiliteDisabledChange < EligibiliteRulesChange
|
||||
def op = :disabled
|
||||
def i18n_params = {}
|
||||
end
|
||||
|
||||
class UpdateEligibiliteMessageChange < EligibiliteRulesChange
|
||||
def op = :message_updated
|
||||
def i18n_params = { ineligibilite_message: @new_revision.ineligibilite_message }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -75,4 +75,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
|
|||
def used_by_routing_rules?
|
||||
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
|
||||
end
|
||||
|
||||
def used_by_ineligibilite_rules?
|
||||
revision.ineligibilite_enabled? && stable_id.in?(revision.ineligibilite_rules&.sources || [])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
34
app/validators/types_de_champ/condition_validator.rb
Normal file
34
app/validators/types_de_champ/condition_validator.rb
Normal file
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
- rendered = render @ineligibilite_rules_component
|
||||
|
||||
- if rendered.present?
|
||||
= turbo_stream.replace dom_id(@procedure.draft_revision, :ineligibilite_rules) do
|
||||
- rendered
|
||||
- else
|
||||
= turbo_stream.remove dom_id(@procedure.draft_revision, :ineligibilite_rules)
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
|||
= render partial: 'update'
|
28
app/views/administrateurs/ineligibilite_rules/edit.html.haml
Normal file
28
app/views/administrateurs/ineligibilite_rules/edit.html.haml
Normal file
|
@ -0,0 +1,28 @@
|
|||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [['Démarches', admin_procedures_path],
|
||||
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
|
||||
['Inéligibilité des dossiers']] }
|
||||
|
||||
|
||||
.fr-container
|
||||
.fr-grid-row
|
||||
.fr-col-12.fr-col-offset-md-2.fr-col-md-8
|
||||
%h1.fr-h1 Inéligibilité des dossiers
|
||||
|
||||
= render Dsfr::AlertComponent.new(title: nil, size: :sm, state: :info, heading_level: 'h2', extra_class_names: 'fr-my-2w') do |c|
|
||||
- c.with_body do
|
||||
%p
|
||||
Les dossiers répondant à vos conditions d’inéligibilité ne pourront pas être déposés. Plus d’informations sur l’inéligibilité des dossiers dans la
|
||||
= link_to('doc', ELIGIBILITE_URL, title: "Document sur l’inéligibilité des dossiers", **external_link_attributes)
|
||||
|
||||
- if !@procedure.draft_revision.conditionable_types_de_champ.present?
|
||||
%p.fr-mt-2w.fr-mb-2w
|
||||
Pour configurer l’inéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions d’inéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire :
|
||||
%ul
|
||||
- Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.each do
|
||||
%li= "« #{t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »"
|
||||
%p.fr-mt-2w
|
||||
= link_to 'Ajouter un champ supportant les conditions d’inéligibilité', champs_admin_procedure_path(@procedure), class: 'fr-link fr-icon-arrow-right-line fr-link--icon-right'
|
||||
= render Procedure::FixedFooterComponent.new(procedure: @procedure)
|
||||
- else
|
||||
= render Conditions::IneligibiliteRulesComponent.new(draft_revision: @procedure.draft_revision)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue