Merge branch 'main' into etq-usager-bouton-jdma

This commit is contained in:
Benoit Queyron 2024-06-11 18:09:01 +02:00 committed by GitHub
commit 97ef01b075
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 4076 additions and 1251 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,34 @@
class Conditions::IneligibiliteRulesComponent < Conditions::ConditionsComponent
include Logic
def initialize(draft_revision:)
@draft_revision = draft_revision
@published_revision = draft_revision.procedure.published_revision
@condition = draft_revision.ineligibilite_rules
@source_tdcs = draft_revision.types_de_champ_for(scope: :public)
end
def pending_changes?
return false if !@published_revision
!@published_revision.compare_ineligibilite_rules(@draft_revision).empty?
end
private
def input_prefix
'procedure_revision[condition_form]'
end
def input_id_for(name, row_index)
"#{@draft_revision.id}-#{name}-#{row_index}"
end
def delete_condition_path(row_index)
delete_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id, row_index:)
end
def add_condition_path
add_row_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id, revision_id: @draft_revision.id)
end
end

View file

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

View file

@ -0,0 +1,49 @@
%div{ id: dom_id(@draft_revision, :ineligibilite_rules) }
= render Procedure::PendingRepublishComponent.new(procedure: @draft_revision.procedure, render_if: pending_changes?)
= render Conditions::ConditionsErrorsComponent.new(conditions: condition_per_row, source_tdcs: @source_tdcs)
.fr-fieldset
= form_for(@draft_revision, url: change_admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), html: { id: 'ineligibilite_form', class: 'width-100' }) do |f|
.fr-fieldset__element
.fr-toggle.fr-toggle--label-left
= f.check_box :ineligibilite_enabled, class: 'fr-toggle__input', data: @opt
= f.label :ineligibilite_enabled, "Bloquer le dépôt des dossiers répondant à des conditions dinéligibilité", data: { 'fr-checked-label': "Activé", 'fr-unchecked-label': "Désactivé" }, class: 'fr-toggle__label'
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :ineligibilite_message, input_type: :text_area, opts: {rows: 5})
.fr-mx-1w.fr-label.fr-py-0.fr-mb-1w.fr-mt-2w
Conditions dinéligibilité
%span.fr-hint-text Vous pouvez utiliser une ou plusieurs condtions pour bloquer le dépot.
.fr-fieldset__element
= form_tag admin_procedure_ineligibilite_rules_path(@draft_revision.procedure_id), method: :patch, data: { turbo: true, controller: 'autosave' }, class: 'form width-100' do
.conditionnel.width-100
%table.condition-table
- if rows.size > 0
%thead
%tr
%th.fr-pt-0.far-left
%th.fr-pt-0.target Champ Cible
%th.fr-pt-0.operator Opérateur
%th.fr-pt-0.value Valeur
%th.fr-pt-0.delete-column
%tbody
- rows.each.with_index do |(targeted_champ, operator_name, value), row_index|
%tr
%td.far-left= far_left_tag(row_index)
%td.target= left_operand_tag(targeted_champ, row_index)
%td.operator= operator_tag(operator_name, targeted_champ, row_index)
%td.value= right_operand_tag(targeted_champ, value, row_index, operator_name)
%td.delete-column= delete_condition_tag(row_index)
%tfoot
%tr
%td.text-right{ colspan: 5 }= add_condition_tag
.padded-fixed-footer
.fixed-footer
.fr-container
.fr-grid-row.fr-col-offset-md-2.fr-col-md-8
.fr-col-12
%ul.fr-btns-group.fr-btns-group--inline-md
%li
= link_to "Annuler et revenir à l'écran de gestion", admin_procedure_path(id: @draft_revision.procedure), class: 'fr-btn fr-btn--secondary', data: { confirm: 'Si vous avez fait des modifications elles ne seront pas sauvegardées.'}
%li
= button_tag "Enregistrer", class: "fr-btn", form: 'ineligibilite_form'

View file

@ -1,4 +1,6 @@
class Dossiers::EditFooterComponent < ApplicationComponent
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

View file

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

View file

@ -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 quinvité, vous pouvez remplir ce formulaire mais <strong>le titulaire du dossier doit le déposer lui-même</strong>.

View file

@ -3,8 +3,13 @@
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
- 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,16 @@
class Dossiers::InvalidIneligibiliteRulesComponent < ApplicationComponent
delegate :can_passer_en_construction?, to: :@dossier
def initialize(dossier:)
@dossier = dossier
@revision = dossier.revision
end
def render?
!can_passer_en_construction?
end
def error_message
@dossier.revision.ineligibilite_message
end
end

View file

@ -0,0 +1,6 @@
fr:
modal:
title: "Your file does not match submission criteria"
close: "Close"
close_alt: "Close this modal"
body: "The procedure « %{procedure_libelle} » have submission criteria, unfortunately your file does not match them. You can not submit your file"

View file

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

View file

@ -0,0 +1,16 @@
%div{ id: dom_id(@dossier, :ineligibilite_rules_broken), data: { controller: 'ineligibilite-rules-match', turbo_force: :server } }
%button.fr-sr-only{ aria: {controls: 'modal-eligibilite-rules-dialog' }, data: {'fr-opened': "false" } }
show modal
%dialog.fr-modal{ "aria-labelledby" => "fr-modal-title-modal-1", role: "dialog", id: 'modal-eligibilite-rules-dialog', data: { 'ineligibilite-rules-match-target' => 'dialog' } }
.fr-container.fr-container--fluid.fr-container-md
.fr-grid-row.fr-grid-row--center
.fr-col-12.fr-col-md-8.fr-col-lg-6
.fr-modal__body
.fr-modal__header
%button.fr-btn--close.fr-btn{ aria: { controls: 'modal-eligibilite-rules-dialog' }, title: t('.modal.close_alt') }= t('.modal.close')
.fr-modal__content
%h1#fr-modal-title-modal-1.fr-modal__title
%span.fr-icon-arrow-right-line.fr-icon--lg>
= t('.modal.title')
%p= error_message

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
class ExpandableErrorList < ApplicationComponent
def initialize(errors:)
@errors = errors
end
def splitted_errors
yield(Array(@errors[0..2]), Array(@errors[3..]))
end
end

View file

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

View file

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

View file

@ -0,0 +1,14 @@
- splitted_errors do |head, tail|
%ul#head-errors.fr-mb-0
- head.each do |error_descriptor|
%li
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
= error_descriptor.error_message
- if tail.size > 0
%button.fr-mt-0.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline{ type: "button", "aria-controls": 'tail-errors', "aria-expanded": "false", class: "" }= t('see_more')
%ul#tail-errors.fr-collapse.fr-mt-0
- tail.each do |error_descriptor|
%li
= link_to error_descriptor.label, error_descriptor.anchor, class: 'error-anchor'
= error_descriptor.error_message

View file

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

View file

@ -0,0 +1,19 @@
class Procedure::Card::IneligibiliteDossierComponent < ApplicationComponent
def initialize(procedure:)
@procedure = procedure
end
def ready?
@procedure.draft_revision
.conditionable_types_de_champ
.present? && @procedure.draft_revision.ineligibilite_enabled
end
def error?
!@procedure.draft_revision.validate(:ineligibilite_rules_editor)
end
def completed?
@procedure.draft_revision.ineligibilite_enabled
end
end

View file

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

View file

@ -0,0 +1,13 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to edit_admin_procedure_ineligibilite_rules_path(@procedure), class: 'fr-tile fr-enlarge-link' do
.fr-tile__body.flex.column.align-center.justify-between
- if !ready?
%p.fr-badge.fr-badge= t('.state.pending')
- elsif error?
%p.fr-badge.fr-badge--error À modifier
- else
%p.fr-badge.fr-badge--success= t('.state.completed')
%div
%h3.fr-h6.fr-mt-10v= t('.title')
%p.fr-tile-subtitle= t('.subtitle')
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')

View file

@ -1,9 +0,0 @@
class Procedure::Card::ModificationsComponent < ApplicationComponent
def initialize(procedure:)
@procedure = procedure
end
def render?
@procedure.revised?
end
end

View file

@ -1,5 +0,0 @@
---
fr:
title:
one: Modification du formulaire
other: Modifications du formulaire

View file

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

View 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

View file

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

View file

@ -0,0 +1,10 @@
class Procedure::PendingRepublishComponent < ApplicationComponent
def initialize(procedure:, render_if:)
@procedure = procedure
@render_if = render_if
end
def render?
@render_if
end
end

View file

@ -0,0 +1,4 @@
---
fr:
pending_republish_html: |
Ces modifications ne seront appliquées qu'à la prochaine publication. Vous pouvez vérifier puis publier les modifications sur l'écran de <a href="%{href}">gestion de la démarche</a>

View file

@ -0,0 +1,3 @@
= render Dsfr::AlertComponent.new(state: :warning) do |c|
- c.with_body do
= t('.pending_republish_html', href: admin_procedure_path(@procedure.id))

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
- list.with_empty do
= 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)

View file

@ -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('leligibilité 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@ class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent
end
def errors?
!errors.empty?
errors.present?
end
def to_html_list(messages)

View file

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

View file

@ -0,0 +1,74 @@
module Administrateurs
class IneligibiliteRulesController < AdministrateurController
before_action :retrieve_procedure
def edit
end
def change
if draft_revision.update(procedure_revision_params)
redirect_to edit_admin_procedure_ineligibilite_rules_path(@procedure)
else
flash[:alert] = draft_revision.errors.full_messages
render :edit
end
end
def add_row
condition = Logic.add_empty_condition_to(draft_revision.ineligibilite_rules)
draft_revision.update!(ineligibilite_rules: condition)
@ineligibilite_rules_component = build_ineligibilite_rules_component
end
def delete_row
condition = condition_form.delete_row(row_index).to_condition
draft_revision.update!(ineligibilite_rules: condition)
@ineligibilite_rules_component = build_ineligibilite_rules_component
end
def update
condition = condition_form.to_condition
draft_revision.update!(ineligibilite_rules: condition)
@ineligibilite_rules_component = build_ineligibilite_rules_component
end
def change_targeted_champ
condition = condition_form.change_champ(row_index).to_condition
draft_revision.update!(ineligibilite_rules: condition)
@ineligibilite_rules_component = build_ineligibilite_rules_component
end
private
def build_ineligibilite_rules_component
Conditions::IneligibiliteRulesComponent.new(draft_revision: draft_revision)
end
def draft_revision
@procedure.draft_revision
end
def condition_form
ConditionForm.new(ineligibilite_rules_params.merge(source_tdcs: draft_revision.types_de_champ_for(scope: :public)))
end
def ineligibilite_rules_params
params
.require(:procedure_revision)
.require(:condition_form)
.permit(:top_operator_name, rows: [:targeted_champ, :operator_name, :value])
end
def row_index
params[:row_index].to_i
end
def procedure_revision_params
params
.require(:procedure_revision)
.permit(:ineligibilite_message, :ineligibilite_enabled)
end
end
end

View file

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

View file

@ -0,0 +1,5 @@
class EmailCheckerController < ApplicationController
def show
render json: EmailChecker.new.check(email: params[:email])
end
end

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

@ -0,0 +1,19 @@
import { ApplicationController } from './application_controller';
declare interface modal {
disclose: () => void;
}
declare interface dsfr {
modal: modal;
}
declare const window: Window &
typeof globalThis & { dsfr: (elem: HTMLElement) => dsfr };
export class InvalidIneligibiliteRulesController extends ApplicationController {
static targets = ['dialog'];
declare dialogTarget: HTMLElement;
connect() {
setTimeout(() => window.dsfr(this.dialogTarget).modal.disclose(), 100);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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") { '' }],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 dun 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 davis
- 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 davis
- 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 davis" },
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 davis" },
class: 'fr-btn fr-btn--secondary'
- else
.blank-tab
%h2.empty-text Aucun expert invité pour le moment.

View file

@ -0,0 +1,7 @@
- rendered = render @ineligibilite_rules_component
- if rendered.present?
= turbo_stream.replace dom_id(@procedure.draft_revision, :ineligibilite_rules) do
- rendered
- else
= turbo_stream.remove dom_id(@procedure.draft_revision, :ineligibilite_rules)

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Inéligibilité des dossiers']] }
.fr-container
.fr-grid-row
.fr-col-12.fr-col-offset-md-2.fr-col-md-8
%h1.fr-h1 Inéligibilité des dossiers
= render Dsfr::AlertComponent.new(title: nil, size: :sm, state: :info, heading_level: 'h2', extra_class_names: 'fr-my-2w') do |c|
- c.with_body do
%p
Les dossiers répondant à vos conditions dinéligibilité ne pourront pas être déposés. Plus dinformations sur linéligibilité des dossiers dans la
= link_to('doc', ELIGIBILITE_URL, title: "Document sur linéligibilité des dossiers", **external_link_attributes)
- if !@procedure.draft_revision.conditionable_types_de_champ.present?
%p.fr-mt-2w.fr-mb-2w
Pour configurer linéligibilité des dossiers, votre formulaire doit comporter au moins un champ supportant les conditions dinéligibilité. Il vous faut donc ajouter au moins un des champs suivant à votre formulaire :
%ul
- Logic::ChampValue::MANAGED_TYPE_DE_CHAMP.values.each do
%li= "« #{t(_1, scope: [:activerecord, :attributes, :type_de_champ, :type_champs])} »"
%p.fr-mt-2w
= link_to 'Ajouter un champ supportant les conditions dinéligibilité', champs_admin_procedure_path(@procedure), class: 'fr-link fr-icon-arrow-right-line fr-link--icon-right'
= render Procedure::FixedFooterComponent.new(procedure: @procedure)
- else
= render Conditions::IneligibiliteRulesComponent.new(draft_revision: @procedure.draft_revision)

Some files were not shown because too many files have changed in this diff Show more