Merge pull request #6052 from betagouv/main

2021-04-06-01
This commit is contained in:
Kara Diaby 2021-04-06 11:41:24 +02:00 committed by GitHub
commit e6faafa314
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 262 additions and 66 deletions

View file

@ -28,3 +28,7 @@
display: inline-block;
width: 5px;
}
.text-center {
text-align: center;
}

View file

@ -9,7 +9,8 @@ module CreateAvisConcern
# the :emails parameter is a 1-element array.
# Hence the call to first
# https://github.com/rails/rails/issues/17225
expert_emails = create_avis_params[:emails].first.split(',').map(&:strip)
expert_emails = create_avis_params[:emails].presence || [].to_json
expert_emails = JSON.parse(expert_emails).map(&:strip).map(&:downcase)
allowed_dossiers = [dossier]
if create_avis_params[:invite_linked_dossiers].present?
@ -65,6 +66,6 @@ module CreateAvisConcern
end
def create_avis_params
params.require(:avis).permit(:introduction_file, :introduction, :confidentiel, :invite_linked_dossiers, emails: [])
params.require(:avis).permit(:introduction_file, :introduction, :confidentiel, :invite_linked_dossiers, :emails)
end
end

View file

@ -60,6 +60,7 @@ module Instructeurs
def avis
@avis_seen_at = current_instructeur.follows.find_by(dossier: dossier)&.avis_seen_at
@avis = Avis.new
@experts_emails = dossier.procedure.experts_procedures.where(revoked_at: nil).map(&:expert).map(&:email).sort
end
def personnes_impliquees

View file

@ -0,0 +1,43 @@
module NewAdministrateur
class ExpertsProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:add_expert_to_procedure, :revoke_expert_from_procedure]
def add_expert_to_procedure
emails = params['emails'].presence || [].to_json
emails = JSON.parse(emails).map(&:strip).map(&:downcase)
valid_users, invalid_users = emails
.map { |email| User.create_or_promote_to_expert(email, SecureRandom.hex) }
.partition(&:valid?)
if invalid_users.any?
flash[:alert] = invalid_users
.filter { |user| user.errors.present? }
.map { |user| "#{user.email} : #{user.errors.full_messages_for(:email).join(', ')}" }
end
if valid_users.present?
valid_users.each do |user|
experts_procedure = ExpertsProcedure.find_or_create_by(expert: user.expert, procedure: @procedure)
if !experts_procedure.revoked_at.nil?
experts_procedure.update!(revoked_at: nil)
end
end
flash[:notice] = t('.experts_assignment',
count: valid_users.count,
value: valid_users.map(&:email).join(', '),
procedure: @procedure.id)
end
redirect_to admin_procedure_invited_expert_list_path(@procedure)
end
def revoke_expert_from_procedure
expert_procedure = ExpertsProcedure.find_by!(procedure: @procedure, id: params[:expert_procedure][:id])
expert_email = expert_procedure.expert.email
expert_procedure.update!(revoked_at: Time.zone.now)
flash[:notice] = "#{expert_email} a été révoqué de la démarche et ne pourra plus déposer d'avis."
redirect_to admin_procedure_invited_expert_list_path(@procedure)
end
end
end

View file

@ -186,7 +186,8 @@ module NewAdministrateur
end
def invited_expert_list
@experts_procedure = @procedure.experts_procedures.sort_by { |expert_procedure| expert_procedure.expert.email }
@experts_procedure = @procedure.experts_procedures.where(revoked_at: nil).sort_by { |expert_procedure| expert_procedure.expert.email }
@experts_emails = experts_procedure_emails
end
def update_allow_decision_access
@ -198,6 +199,10 @@ module NewAdministrateur
private
def experts_procedure_emails
@procedure.experts.map(&:email).sort
end
def apercu_tab
params[:tab] || 'dossier'
end

View file

@ -360,12 +360,12 @@ module Users
if champs_params[:dossier]
@dossier.assign_attributes(champs_params[:dossier])
# FIXME in some cases a removed repetition bloc row is submitted.
# In this case it will be trated as a new records and action will fail.
# FIXME: in some cases a removed repetition bloc row is submitted.
# In this case it will be treated as a new record, and the action will fail.
@dossier.champs.filter(&:repetition?).each do |champ|
champ.champs = champ.champs.filter(&:persisted?)
end
if @dossier.champs.any?(&:changed?)
if @dossier.champs.any?(&:changed_for_autosave?)
@dossier.last_champ_updated_at = Time.zone.now
end
if !@dossier.save

View file

@ -18,7 +18,7 @@
# type_de_champ_id :integer
#
class Champ < ApplicationRecord
belongs_to :dossier, -> { with_discarded }, inverse_of: :champs, touch: true, optional: false
belongs_to :dossier, -> { with_discarded }, inverse_of: false, touch: true, optional: false
belongs_to :type_de_champ, inverse_of: :champ, optional: false
belongs_to :parent, class_name: 'Champ', optional: true
has_many :commentaires

View file

@ -65,8 +65,8 @@ class Dossier < ApplicationRecord
has_one_attached :justificatif_motivation
has_one_attached :pdf_export_for_instructeur
has_many :champs, -> { root.public_ordered }, inverse_of: :dossier, dependent: :destroy
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy
has_many :champs, -> { root.public_ordered }, inverse_of: false, dependent: :destroy
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: false, dependent: :destroy
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
has_many :invites, dependent: :destroy
has_many :follows, -> { active }, inverse_of: :dossier

View file

@ -4,6 +4,7 @@
#
# id :bigint not null, primary key
# allow_decision_access :boolean default(FALSE), not null
# revoked_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# expert_id :bigint not null

View file

@ -4,7 +4,7 @@
#
# id :bigint not null, primary key
# format :string not null
# key :text
# key :text not null
# created_at :datetime not null
# updated_at :datetime not null
#

View file

@ -1,9 +1,17 @@
- if @dossier.procedure.feature_enabled?(:admin_affect_experts_to_avis).blank?
%section.ask-avis
%h1.tab-title Inviter des personnes à donner leur avis
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
= form_for avis, url: url, html: { class: 'form' } do |f|
= f.email_field :emails, placeholder: 'Adresses email, séparées par des virgules', required: true, multiple: true, onchange: "javascript:DS.replaceSemicolonByComma(event);"
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag 'avis[emails]', nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: [],
selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'avis_emails',
acceptNewValues: true)
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true
%p.tab-title Ajouter une pièce jointe
.form-group

View file

@ -1,9 +1,22 @@
%section.ask-avis
%h1.tab-title Inviter des personnes à donner leur avis
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
- if @dossier.procedure.feature_enabled?(:admin_affect_experts_to_avis)
%p.avis-notice Choisissez des experts à qui vous souhaitez demander un avis parmi la liste prédéfinie par les administrateurs de la démarche
- else
%p.avis-notice Entrez les adresses email des experts à qui vous souhaitez demander un avis
= form_for avis, url: url, html: { class: 'form' } do |f|
= f.email_field :emails, placeholder: 'Adresses email, séparées par des virgules', required: true, multiple: true, onchange: "javascript:DS.replaceSemicolonByComma(event);"
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag 'avis[emails]', nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: @dossier.procedure.feature_enabled?(:admin_affect_experts_to_avis) ? @experts_emails : [],
selected: [],
disabled: [],
hiddenFieldId: hidden_field_id,
label: 'avis_emails',
id: 'avis_emails',
acceptNewValues: @dossier.procedure.feature_enabled?(:admin_affect_experts_to_avis).blank?)
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true
%p.tab-title Ajouter une pièce jointe
.form-group

View file

@ -5,12 +5,35 @@
.container
%h1.page-title.mt-2 Experts invités sur #{@procedure.libelle}
.container.groupe-instructeur
.card
.card-title Affecter des experts à la démarche
= form_for :experts_procedure,
url: admin_procedure_add_expert_to_procedure_path(@procedure),
html: { class: 'form' } do |f|
.instructeur-wrapper
%p.notice Pendant l'instruction d'un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
%p.notice Entrez les adresses email des experts 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: [],
selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'email expert',
acceptNewValues: true)
= f.submit 'Affecter à la démarche', class: 'button primary send'
- if @experts_procedure.present?
%table.table.mt-2
%thead
%tr
%th Liste des experts
- if @procedure.feature_enabled?(:make_experts_notifiable)
%th Nombre d'avis
- if @procedure.feature_enabled?(:admin_affect_experts_to_avis)
%th Notifier des décisions sur les dossiers
%tbody
- @experts_procedure.each do |expert_procedure|
@ -18,8 +41,10 @@
%td
%span.icon.person
= expert_procedure.expert.email
- if @procedure.feature_enabled?(:make_experts_notifiable)
%td
%td.text-center
= expert_procedure.avis.count
- if @procedure.feature_enabled?(:admin_affect_experts_to_avis)
%td.text-center
= form_for expert_procedure,
url: admin_procedure_update_allow_decision_access_path(expert_procedure: expert_procedure),
remote: true,
@ -31,6 +56,12 @@
%span.toggle-switch-control.round
%span.toggle-switch-label.on
%span.toggle-switch-label.off
%td.actions= button_to 'retirer',
{ action: "revoke_expert_from_procedure", :controller=>"new_administrateur/experts_procedures" },
{ method: :put,
data: { confirm: "Êtes-vous sûr de vouloir révoquer l'expert « #{expert_procedure.expert.email} » de la démarche #{expert_procedure.procedure.libelle} ? Les instructeurs ne pourront plus lui demander d'avis" },
params: { expert_procedure: { id: expert_procedure.id }},
class: 'button' }
- else
.blank-tab
%h2.empty-text Aucun expert invité pour le moment.

View file

@ -1,5 +1,12 @@
fr:
new_administrateur:
experts_procedures:
wrong_address:
one: "%{value} nest pas une adresse email valide"
other: "%{value} ne sont pas des adresses emails valides"
experts_assignment:
one: "L'expert %{value} a été affecté à la démarche n° %{procedure}"
other: "Les experts %{value} ont été affectés à la démarche n° %{procedure}"
groupe_instructeurs:
index:
existing_groupe:

View file

@ -408,7 +408,8 @@ Rails.application.routes.draw do
post 'transfer' => 'procedures#transfer', as: :transfer
get 'invited_expert_list'
put 'update_allow_decision_access' => 'procedures#update_allow_decision_access', as: :update_allow_decision_access
post 'add_expert_to_procedure' => 'experts_procedures#add_expert_to_procedure', as: :add_expert_to_procedure
put 'revoke_expert_from_procedure' => 'experts_procedures#revoke_expert_from_procedure', as: :revoke_expert_from_procedure
resources :mail_templates, only: [:edit, :update]
resources :groupe_instructeurs, only: [:index, :show, :create, :update, :destroy] do

View file

@ -0,0 +1,5 @@
class AddRevokedAtToExpertsProcedures < ActiveRecord::Migration[6.1]
def change
add_column :experts_procedures, :revoked_at, :datetime
end
end

View file

@ -0,0 +1,5 @@
class ExportsKeyNotNull < ActiveRecord::Migration[6.1]
def change
change_column_null :exports, :key, false
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_03_31_123709) do
ActiveRecord::Schema.define(version: 2021_04_02_163003) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -345,6 +345,7 @@ ActiveRecord::Schema.define(version: 2021_03_31_123709) do
t.boolean "allow_decision_access", default: false, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "revoked_at"
t.index ["expert_id", "procedure_id"], name: "index_experts_procedures_on_expert_id_and_procedure_id", unique: true
t.index ["expert_id"], name: "index_experts_procedures_on_expert_id"
t.index ["procedure_id"], name: "index_experts_procedures_on_procedure_id"
@ -354,7 +355,7 @@ ActiveRecord::Schema.define(version: 2021_03_31_123709) do
t.string "format", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "key"
t.text "key", null: false
t.index ["format", "key"], name: "index_exports_on_format_and_key", unique: true
end

View file

@ -159,7 +159,7 @@ describe Experts::AvisController, type: :controller do
let(:previous_avis_confidentiel) { false }
let(:asked_confidentiel) { false }
let(:intro) { 'introduction' }
let(:emails) { ["toto@totomail.com"] }
let(:emails) { "[\"toto@totomail.com\"]" }
let(:invite_linked_dossiers) { nil }
before do
@ -176,7 +176,7 @@ describe Experts::AvisController, type: :controller do
describe '#create_avis' do
let!(:previous_avis) { create(:avis, dossier: dossier, claimant: claimant, experts_procedure: experts_procedure, confidentiel: previous_avis_confidentiel) }
let(:emails) { ['a@b.com'] }
let(:emails) { "[\"a@b.com\"]" }
let(:intro) { 'introduction' }
let(:created_avis) { Avis.last }
let!(:old_avis_count) { Avis.count }
@ -194,7 +194,7 @@ describe Experts::AvisController, type: :controller do
context 'when an invalid email' do
let(:previous_avis_confidentiel) { false }
let(:asked_confidentiel) { false }
let(:emails) { ["toto.fr"] }
let(:emails) { "[\"toto.fr\"]" }
it { expect(response).to render_template :instruction }
it { expect(flash.alert).to eq(["toto.fr : Email n'est pas valide"]) }
@ -205,7 +205,7 @@ describe Experts::AvisController, type: :controller do
context 'ask review with attachment' do
let(:previous_avis_confidentiel) { false }
let(:asked_confidentiel) { false }
let(:emails) { ["toto@totomail.com"] }
let(:emails) { "[\"toto@totomail.com\"]" }
it { expect(created_avis.introduction_file).to be_attached }
it { expect(created_avis.introduction_file.filename).to eq("piece_justificative_0.pdf") }
@ -216,7 +216,7 @@ describe Experts::AvisController, type: :controller do
context 'with multiple emails' do
let(:asked_confidentiel) { false }
let(:previous_avis_confidentiel) { false }
let(:emails) { ["toto.fr,titi@titimail.com"] }
let(:emails) { "[\"toto.fr\",\"titi@titimail.com\"]" }
it { expect(response).to render_template :instruction }
it { expect(flash.alert).to eq(["toto.fr : Email n'est pas valide"]) }

View file

@ -448,7 +448,7 @@ describe Instructeurs::DossiersController, type: :controller do
}
end
let(:emails) { ['email@a.com'] }
let(:emails) { "[\"email@a.com\"]" }
context "notifications updates" do
context 'when an instructeur follows the dossier' do
@ -476,7 +476,7 @@ describe Instructeurs::DossiersController, type: :controller do
it { expect(response).to redirect_to(avis_instructeur_dossier_path(dossier.procedure, dossier)) }
context "with an invalid email" do
let(:emails) { ['emaila.com'] }
let(:emails) { "[\"emaila.com\"]" }
before { subject }
@ -487,7 +487,7 @@ describe Instructeurs::DossiersController, type: :controller do
end
context 'with multiple emails' do
let(:emails) { ["toto.fr,titi@titimail.com"] }
let(:emails) { "[\"toto.fr\",\"titi@titimail.com\"]" }
before { subject }

View file

@ -0,0 +1,49 @@
describe NewAdministrateur::ExpertsProceduresController, type: :controller do
let(:admin) { create(:administrateur) }
before do
sign_in(admin.user)
end
describe '#add_expert_to_procedure' do
let(:procedure) { create :procedure, administrateur: admin }
let(:expert) { create(:expert) }
let(:expert2) { create(:expert) }
subject do
post :add_expert_to_procedure,
params: { procedure_id: procedure.id, emails: "[\"#{expert.email}\",\"#{expert2.email}\"]" }
end
before do
subject
end
context 'of multiple experts' do
it { expect(procedure.experts.include?(expert)).to be_truthy }
it { expect(procedure.experts.include?(expert2)).to be_truthy }
it { expect(flash.notice).to be_present }
it { expect(response).to redirect_to(admin_procedure_invited_expert_list_path(procedure)) }
end
end
describe '#revoke_expert_from_procedure' do
let(:procedure) { create :procedure, administrateur: admin }
let(:expert) { create(:expert) }
let(:expert_procedure) { ExpertsProcedure.create(expert: expert, procedure: procedure) }
subject do
put :revoke_expert_from_procedure, params: { procedure_id: procedure.id, expert_procedure: { id: expert_procedure.id } }
end
before do
subject
expert_procedure.reload
end
context 'of multiple experts' do
it { expect(expert_procedure.revoked_at).to be_present }
it { expect(flash.notice).to be_present }
it { expect(response).to redirect_to(admin_procedure_invited_expert_list_path(procedure)) }
end
end
end

View file

@ -619,13 +619,14 @@ describe Users::DossiersController, type: :controller do
it 'updates the champs' do
subject
expect(first_champ.reload.value).to eq('beautiful value')
expect(first_champ.dossier.reload.last_champ_updated_at).to eq(now)
expect(piece_justificative_champ.reload.piece_justificative_file).to be_attached
end
it 'updates the dossier modification date' do
it 'updates the dossier timestamps' do
subject
expect(dossier.reload.updated_at.year).to eq(2100)
dossier.reload
expect(dossier.updated_at).to eq(now)
expect(dossier.last_champ_updated_at).to eq(now)
end
it 'updates the dossier state' do
@ -635,22 +636,34 @@ describe Users::DossiersController, type: :controller do
it { is_expected.to redirect_to(demande_dossier_path(dossier)) }
context 'when only files champs are modified' do
context 'when only a single file champ are modified' do
# A bug in ActiveRecord causes records changed through grand-parent <-> parent <-> child
# relationships do not touch the grand-parent record on change.
# This situation is hit when updating just the attachment of a champ (and not the
# champ itself).
#
# This test ensures that, whatever workaround we wrote for this, it still works properly.
#
# See https://github.com/rails/rails/issues/26726
let(:submit_payload) do
{
id: dossier.id,
dossier: {
champs_attributes: {
champs_attributes: [
{
id: piece_justificative_champ.id,
piece_justificative_file: file
}
]
}
}
end
it 'updates the dossier modification date' do
it 'updates the dossier timestamps' do
subject
expect(dossier.reload.updated_at.year).to eq(2100)
dossier.reload
expect(dossier.updated_at).to eq(now)
expect(dossier.last_champ_updated_at).to eq(now)
end
end
end
@ -667,6 +680,12 @@ describe Users::DossiersController, type: :controller do
it { expect(response).to render_template(:modifier) }
it { expect(flash.alert).to eq(['nop']) }
it 'does not update the dossier timestamps' do
dossier.reload
expect(dossier.updated_at).not_to eq(now)
expect(dossier.last_champ_updated_at).not_to eq(now)
end
it 'does not send an email' do
expect(NotificationMailer).not_to receive(:send_initiated_notification)

View file

@ -1,4 +1,4 @@
feature 'Inviting an expert:' do
feature 'Inviting an expert:', js: true do
include ActiveJob::TestHelper
include ActionView::Helpers
@ -21,7 +21,7 @@ feature 'Inviting an expert:' do
click_on 'Avis externes'
expect(page).to have_current_path(avis_instructeur_dossier_path(procedure, dossier))
fill_in 'avis_emails', with: "#{expert.email}, #{expert2.email}"
page.execute_script("document.querySelector('#avis_emails').value = '[\"#{expert.email}\",\"#{expert2.email}\"]'")
fill_in 'avis_introduction', with: 'Bonjour, merci de me donner votre avis sur ce dossier.'
check 'avis_invite_linked_dossiers'
page.select 'confidentiel', from: 'avis_confidentiel'

View file

@ -1,4 +1,4 @@
feature 'Instructing a dossier:' do
feature 'Instructing a dossier:', js: true do
include ActiveJob::TestHelper
let(:password) { 'my-s3cure-p4ssword' }
@ -101,11 +101,13 @@ feature 'Instructing a dossier:' do
click_on 'Avis externes'
expect(page).to have_current_path(avis_instructeur_dossier_path(procedure, dossier))
expert_email_formated = "[\"expert@tps.com\"]"
expert_email = 'expert@tps.com'
ask_confidential_avis(expert_email, 'a good introduction')
ask_confidential_avis(expert_email_formated, 'a good introduction')
expert_email_formated = "[\"#{instructeur2.email}\"]"
expert_email = instructeur2.email
ask_confidential_avis(expert_email, 'a good introduction')
ask_confidential_avis(expert_email_formated, 'a good introduction')
click_on 'Personnes impliquées'
expect(page).to have_text(expert_email)
@ -206,7 +208,7 @@ feature 'Instructing a dossier:' do
end
def ask_confidential_avis(to, introduction)
fill_in 'avis_emails', with: to
page.execute_script("document.querySelector('#avis_emails').value = '#{to}'")
fill_in 'avis_introduction', with: introduction
select 'confidentiel', from: 'avis_confidentiel'
click_on 'Demander un avis'