Merge pull request #6469 from tchak/feat-routage-pour-tous

Routage pour tous !
This commit is contained in:
Paul Chavard 2021-09-18 12:27:42 +02:00 committed by GitHub
commit ba1a898b0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 201 additions and 149 deletions

View file

@ -11,7 +11,15 @@ module NewAdministrateur
def index
@procedure = procedure
@groupes_instructeurs = paginated_groupe_instructeurs
if procedure.routee?
@groupes_instructeurs = paginated_groupe_instructeurs
@instructeurs = []
@available_instructeur_emails = []
else
@groupes_instructeurs = []
@instructeurs = paginated_instructeurs
@available_instructeur_emails = available_instructeur_emails
end
end
def show
@ -131,15 +139,17 @@ module NewAdministrateur
else
if instructeurs.present?
instructeurs.each do |instructeur|
instructeur.assign_to_procedure(procedure)
end
procedure.defaut_groupe_instructeur.instructeurs << instructeurs
flash[:notice] = "Les instructeurs ont bien été affectés à la démarche"
end
end
end
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
if procedure.routee?
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
else
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
end
end
def remove_instructeur
@ -164,7 +174,12 @@ module NewAdministrateur
end
end
end
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
if procedure.routee?
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
else
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
end
end
def update_routing_criteria_name
@ -174,6 +189,13 @@ module NewAdministrateur
notice: "Le libellé est maintenant « #{procedure.routing_criteria_name} »."
end
def update_routing_enabled
procedure.update!(routing_enabled: true)
redirect_to admin_procedure_groupe_instructeurs_path(procedure),
notice: "Le routage est activé."
end
def import
if !CSV_ACCEPTED_CONTENT_TYPES.include?(group_csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type)
flash[:alert] = "Importation impossible : veuillez importer un fichier CSV"
@ -227,7 +249,11 @@ module NewAdministrateur
end
def groupe_instructeur
procedure.groupe_instructeurs.find(params[:id])
if params[:id].present?
procedure.groupe_instructeurs.find(params[:id])
else
procedure.defaut_groupe_instructeur
end
end
def instructeur_id

View file

@ -73,19 +73,15 @@ class Instructeur < ApplicationRecord
end
def assign_to_procedure(procedure)
begin
assign_to.create({
procedure: procedure,
groupe_instructeur: procedure.defaut_groupe_instructeur
})
true
rescue ActiveRecord::RecordNotUnique
false
if !procedure.defaut_groupe_instructeur.in?(groupe_instructeurs)
groupe_instructeurs << procedure.defaut_groupe_instructeur
end
end
def remove_from_procedure(procedure)
!!(procedure.defaut_groupe_instructeur.in?(groupe_instructeurs) && groupe_instructeurs.destroy(procedure.defaut_groupe_instructeur))
if procedure.defaut_groupe_instructeur.in?(groupe_instructeurs)
groupe_instructeurs.destroy(procedure.defaut_groupe_instructeur)
end
end
def last_week_overview

View file

@ -35,6 +35,7 @@
# path :string not null
# published_at :datetime
# routing_criteria_name :text default("Votre ville")
# routing_enabled :boolean
# test_started_at :datetime
# unpublished_at :datetime
# web_hook_url :string
@ -628,7 +629,7 @@ class Procedure < ApplicationRecord
end
def routee?
groupe_instructeurs.size > 1
routing_enabled? || groupe_instructeurs.size > 1
end
def defaut_groupe_instructeur_for_new_dossier

View file

@ -0,0 +1,52 @@
.card
= form_for procedure,
url: { action: :update_routing_criteria_name },
html: { class: 'form' } do |f|
= f.label :routing_criteria_name do
Libellé du routage
%p.notice Ce texte apparaitra sur le formulaire usager comme le libellé dune liste
= f.text_field :routing_criteria_name, placeholder: 'ex. Votre ville', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des Groupes
= form_for :groupe_instructeur, html: { class: 'form' } do |f|
= f.label :label do
Ajouter un groupe
%p.notice Ce groupe sera un choix de la liste « #{procedure.routing_criteria_name} » .
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
- csv_max_size = NewAdministrateur::GroupeInstructeursController::CSV_MAX_SIZE
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t(".existing_groupe", count: groupes_instructeurs.total_count)
%tbody
- groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= link_to "voir", admin_procedure_groupe_instructeur_path(procedure, group)
- if groupes_instructeurs.many?
- if group.dossiers.empty?
%td.actions
= link_to admin_procedure_groupe_instructeur_path(procedure, group), { method: :delete, class: 'button', data: { confirm: "Êtes-vous sûr de vouloir supprimer le groupe « #{group.label} » ?" }} do
%span.icon.delete
supprimer ce groupe
- else
%td.actions
= link_to reaffecter_dossiers_admin_procedure_groupe_instructeur_path(procedure, group), class: 'button', title:'Réaffecter les dossiers à un autre groupe afin de pouvoir le supprimer' do
%span.icon.follow
déplacer les dossiers
= paginate groupes_instructeurs

View file

@ -0,0 +1,37 @@
.card
.card-title Affectation des instructeurs
= form_for :instructeur, url: { action: :add_instructeur, id: groupe_instructeur.id }, html: { class: 'form' } do |f|
.instructeur-wrapper
- if !procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: available_instructeur_emails, selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'email instructeur',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t('.assigned_instructeur', count: instructeurs.count)
%tbody
- instructeurs.each do |instructeur|
%tr
%td
%span.icon.person
#{instructeur.email}
- confirmation_message = procedure.routee? ? "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » du groupe « #{groupe_instructeur.label} » ?" : "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » de la démarche ?"
%td.actions= button_to 'retirer',
{ action: :remove_instructeur, id: groupe_instructeur.id },
{ method: :delete,
data: { confirm: confirmation_message },
params: { instructeur: { id: instructeur.id }},
class: 'button' }
= paginate instructeurs

View file

@ -0,0 +1,5 @@
.card
.card-title Routage
%p.notice= t('.notice_html')
= link_to 'Activer le routage', update_routing_enabled_admin_procedure_groupe_instructeurs_path(procedure), class: 'button primary', method: 'patch'

View file

@ -1,57 +1,22 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Groupes dinstructeurs'] }
- if @procedure.routee?
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Groupes dinstructeurs'] }
- else
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Instructeurs'] }
.container.groupe-instructeur
.card
= form_for @procedure,
url: { action: :update_routing_criteria_name },
html: { class: 'form' } do |f|
- if @procedure.routee?
= render partial: 'new_administrateur/groupe_instructeurs/edit', locals: { procedure: @procedure, groupes_instructeurs: @groupes_instructeurs }
- else
= render partial: 'new_administrateur/groupe_instructeurs/routing', locals: { procedure: @procedure }
= render partial: 'new_administrateur/groupe_instructeurs/instructeurs',
locals: { procedure: @procedure,
groupe_instructeur: @procedure.defaut_groupe_instructeur,
instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails }
= f.label :routing_criteria_name do
Libellé du routage
%p.notice Ce texte apparaitra sur le formulaire usager comme le libellé dune liste
= f.text_field :routing_criteria_name, placeholder: 'ex. Votre ville', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des Groupes
= form_for :groupe_instructeur, html: { class: 'form' } do |f|
= f.label :label do
Ajouter un groupe
%p.notice Ce groupe sera un choix de la liste « #{@procedure.routing_criteria_name} » .
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
- csv_max_size = NewAdministrateur::GroupeInstructeursController::CSV_MAX_SIZE
= form_tag import_admin_procedure_groupe_instructeurs_path(@procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t(".existing_groupe", count: @groupes_instructeurs.total_count)
%tbody
- @groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= link_to "voir", admin_procedure_groupe_instructeur_path(@procedure, group)
- if @groupes_instructeurs.many?
- if group.dossiers.empty?
%td.actions
= link_to admin_procedure_groupe_instructeur_path(@procedure, group), { method: :delete, class: 'button', data: { confirm: "Êtes-vous sûr de vouloir supprimer le groupe « #{group.label} » ?" }} do
%span.icon.delete
supprimer ce groupe
- else
%td.actions
= link_to reaffecter_dossiers_admin_procedure_groupe_instructeur_path(@procedure, group), class: 'button', title:'Réaffecter les dossiers à un autre groupe afin de pouvoir le supprimer' do
%span.icon.follow
déplacer les dossiers
= paginate @groupes_instructeurs

View file

@ -1,55 +1,14 @@
- if feature_enabled?(:administrateur_routage)
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to('Groupes dinstructeurs', admin_procedure_groupe_instructeurs_path(@procedure)),
@groupe_instructeur.label] }
- else
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Instructeurs'] }
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to('Groupes dinstructeurs', admin_procedure_groupe_instructeurs_path(@procedure)),
@groupe_instructeur.label] }
.container.groupe-instructeur
- if feature_enabled?(:administrateur_routage)
= render partial: 'new_administrateur/groups_header'
.card
.card-title Affectation des instructeurs
= form_for :instructeur,
url: { action: :add_instructeur },
html: { class: 'form' } do |f|
.instructeur-wrapper
- if !@procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: @available_instructeur_emails, selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'email instructeur',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t('.assigned_instructeur', count: @instructeurs.count)
%tbody
- @instructeurs.each do |instructeur|
%tr
%td
%span.icon.person
#{instructeur.email}
%td.actions= button_to 'retirer',
{ action: :remove_instructeur },
{ method: :delete,
data: { confirm: feature_enabled?(:administrateur_routage) ? "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » du groupe  « #{@groupe_instructeur.label} » ?" : "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » de la démarche ?" },
params: { instructeur: { id: instructeur.id }},
class: 'button' }
= paginate @instructeurs
= render partial: 'new_administrateur/groups_header'
= render partial: 'new_administrateur/groupe_instructeurs/instructeurs',
locals: { procedure: @procedure,
groupe_instructeur: @groupe_instructeur,
instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails }

View file

@ -18,7 +18,7 @@
.admin-procedures-list-row.actions.flex.justify-between
%div
- if feature_enabled?(:administrateur_routage)
- if procedure.routee?
%span.icon.person
%span.badge.baseline= procedure.groupe_instructeurs.count
- else

View file

@ -107,14 +107,8 @@
%p.card-admin-subtitle Gestion de la démarche
%p.button Modifier
- if feature_enabled?(:administrateur_routage)
- instructeur_link = admin_procedure_groupe_instructeurs_path(@procedure)
- else
- instructeur_link = admin_procedure_groupe_instructeur_path(@procedure, @procedure.defaut_groupe_instructeur)
= link_to instructeur_link, id: 'groupe-instructeurs', class: 'card-admin' do
- if feature_enabled?(:administrateur_routage) || @procedure.instructeurs.count > 1
= link_to admin_procedure_groupe_instructeurs_path(@procedure), id: 'groupe-instructeurs', class: 'card-admin' do
- if @procedure.routee? || @procedure.instructeurs.count > 1
%div
%span.icon.accept
%p.card-admin-status-accept Validé
@ -124,12 +118,12 @@
%p.card-admin-status-todo À faire
%div
%p.card-admin-title
- if feature_enabled?(:administrateur_routage)
- if @procedure.routee?
%span.badge.baseline= @procedure.groupe_instructeurs.count
- else
%span.badge.baseline= @procedure.instructeurs.count
= feature_enabled?(:administrateur_routage) ? "Groupe Instructeurs" : "#{"Instructeur".pluralize(@procedure.instructeurs.count)}"
= @procedure.routee? ? "Groupe Instructeurs" : "#{"Instructeur".pluralize(@procedure.instructeurs.count)}"
%p.card-admin-subtitle Suivi des dossiers
%p.button Modifier

View file

@ -25,7 +25,6 @@ end
# A list of features to be deployed on first push
features = [
:administrateur_routage,
:administrateur_web_hook,
:api_particulier,
:dossier_pdf_vide,

View file

@ -13,10 +13,6 @@ fr:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
show:
assigned_instructeur:
one: "%{count} instructeur est affecté"
other: "%{count} instructeurs sont affectés"
add_instructeur:
wrong_address:
one: "%{value} nest pas une adresse email valide"
@ -28,3 +24,18 @@ fr:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
instructeurs:
assigned_instructeur:
one: "%{count} instructeur est affecté"
other: "%{count} instructeurs sont affectés"
edit:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
routing:
notice_html: |
Le routage est une fonctionnalité pour les démarches nécessitant le partage de linstruction entre différents groupes en fonction dun critère précis (territoire, thématique ou autre).
<br><br>
Cette fonctionnalité permet dacheminer les dossier vers chaque groupe, et de ne plus avoir besoin de filtrer ses dossiers parmi une grande quantité de demandes. Elle est donc particulièrement adaptée pour les démarches nationales instruites localement.
<br><br>
Les instructeurs ne voient que les dossiers les concernant, et nont donc pas accès aux données extérieures à leur périmètre.

View file

@ -422,6 +422,7 @@ Rails.application.routes.draw do
collection do
patch 'update_routing_criteria_name'
patch 'update_routing_enabled'
post 'import'
end
end

View file

@ -0,0 +1,5 @@
class AddRoutingEnabledToProcedures < ActiveRecord::Migration[6.1]
def change
add_column :procedures, :routing_enabled, :boolean
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_09_08_170019) do
ActiveRecord::Schema.define(version: 2021_09_15_170019) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -622,6 +622,7 @@ ActiveRecord::Schema.define(version: 2021_09_08_170019) do
t.text "api_particulier_scopes", default: [], array: true
t.jsonb "api_particulier_sources", default: {}
t.index ["api_particulier_sources"], name: "index_procedures_on_api_particulier_sources", using: :gin
t.boolean "routing_enabled"
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id"
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"

View file

@ -2,7 +2,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
render_views
let(:admin) { create(:administrateur) }
let(:procedure) { create(:procedure, :published, :for_individual, administrateurs: [admin]) }
let(:procedure) { create(:procedure, :published, :for_individual, administrateurs: [admin], routing_enabled: true) }
let!(:gi_1_1) { procedure.defaut_groupe_instructeur }
let(:procedure2) { create(:procedure, :published) }
@ -222,7 +222,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(response.status).to eq(200) }
it { expect(subject.request.flash[:alert]).to be_nil }
it { expect(subject.request.flash[:notice]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
context 'when there is at least one bad email' do
@ -230,13 +230,13 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(response.status).to eq(200) }
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject.request.flash[:notice]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
context 'when the admin wants to assign an instructor who is already assigned on this procedure' do
let(:emails) { ['instructeur_1@ministere_a.gouv.fr'].to_json }
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
end
@ -344,7 +344,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(subject.request.flash[:notice]).to be_present }
it { expect(subject.request.flash[:alert]).to be_nil }
it { expect(response.status).to eq(302) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, gi_1_1) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
context 'when the instructor is not assigned to the procedure' do
@ -352,7 +352,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject.request.flash[:notice]).to be_nil }
it { expect(response.status).to eq(302) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, gi_1_1) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
end

View file

@ -6,8 +6,8 @@ feature 'The routing', js: true do
let(:litteraire_user) { create(:user, password: password) }
before do
procedure.update(routing_enabled: true)
procedure.defaut_groupe_instructeur.instructeurs << administrateur.instructeur
Flipper.enable_actor(:administrateur_routage, administrateur.user)
end
scenario 'works' do
@ -32,14 +32,14 @@ feature 'The routing', js: true do
# add victor to littéraire groupe
find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
expect(page).to have_text("Linstructeur victor@inst.com a été affecté au groupe « littéraire »")
victor = User.find_by(email: 'victor@inst.com').instructeur
# add superwoman to littéraire groupe
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
expect(page).to have_text("Linstructeur superwoman@inst.com a été affecté au groupe « littéraire »")
superwoman = User.find_by(email: 'superwoman@inst.com').instructeur