feat(dossier): fork dossier when editing en construction

This commit is contained in:
Paul Chavard 2023-03-21 18:24:39 +01:00 committed by Colin Darie
parent 025bd5beaf
commit 08a2a2c9aa
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
20 changed files with 193 additions and 99 deletions

View file

@ -3,10 +3,10 @@
%span.autosave-explanation-text
- if annotation?
= t('.annotations.explanation')
- elsif dossier.brouillon?
= t('.brouillon.explanation')
- else
- elsif dossier.editing_fork?
= t('.en_construction.explanation')
- else
= t('.brouillon.explanation')
- if !annotation?
= link_to t('.more_information'), t("links.common.faq.autosave_url"), class: 'autosave-more-infos fr-link fr-link--sm', **external_link_attributes
@ -15,10 +15,10 @@
%span.autosave-label
- if annotation?
= t('.annotations.confirmation')
- elsif dossier.brouillon?
= t('.brouillon.confirmation')
- else
- elsif dossier.editing_fork?
= t('.en_construction.confirmation')
- else
= t('.brouillon.confirmation')
- if !annotation?
= link_to t('.more_information'), t("links.common.faq.autosave_url"), class: 'autosave-more-infos fr-link fr-link--sm', **external_link_attributes
@ -27,10 +27,10 @@
%span.autosave-label
- if annotation?
= t('.annotations.error')
- elsif dossier.brouillon?
= t('.brouillon.error')
- else
- elsif dossier.editing_fork?
= t('.en_construction.error')
- else
= t('.brouillon.error')
%button.button.small.autosave-retry{ type: :button, data: { action: 'autosave-status#onClickRetryButton', autosave_status_target: 'retryButton' } }
%span.autosave-retry-label réessayer
%span.autosave-retrying-label enregistrement en cours…

View file

@ -14,7 +14,7 @@ class Dossiers::EditFooterComponent < ApplicationComponent
@annotation.present?
end
def button_options
def submit_draft_button_options
{
class: 'fr-btn fr-btn--sm',
disabled: !owner?,
@ -23,6 +23,14 @@ class Dossiers::EditFooterComponent < ApplicationComponent
}
end
def submit_en_construction_button_options
{
class: 'fr-btn fr-btn--sm',
method: :post,
data: { 'disable-with': t('.submitting'), controller: 'autosave-submit' }
}
end
def render?
!@dossier.for_procedure_preview?
end

View file

@ -1,5 +1,6 @@
---
en:
submit: Submit the file
submit_changes: Submit file changes
submitting: Submitting…
invite_notice: You are invited to make amendments to this file but only the owner themselves can submit it.

View file

@ -1,5 +1,6 @@
---
fr:
submit: Déposer le dossier
submit_changes: Déposer les modifications
submitting: Envoi en cours…
invite_notice: En tant quinvité, vous pouvez remplir ce formulaire mais le titulaire du dossier doit le déposer lui-même.

View file

@ -3,7 +3,10 @@
= render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
- if !annotation? && @dossier.can_transition_to_en_construction?
= button_to t('.submit'), brouillon_dossier_url(@dossier), button_options
= button_to t('.submit'), brouillon_dossier_url(@dossier), submit_draft_button_options
- elsif @dossier.forked_with_changes?
= button_to t('.submit_changes'), modifier_dossier_url(@dossier.editing_fork_origin), submit_en_construction_button_options
- if @dossier.brouillon? && !owner?
.send-notice.invite-cannot-submit

View file

@ -3,7 +3,7 @@
= @form.label @champ.main_value_name, id: @champ.labelledby_id, for: @champ.input_id do
- render EditableChamp::ChampLabelContentComponent.new champ: @champ, seen_at: @seen_at
- else
.form-label.mb-4
.form-label.mb-4{ id: @champ.labelledby_id }
= render EditableChamp::ChampLabelContentComponent.new champ: @champ, seen_at: @seen_at
- if @champ.description.present?

View file

@ -2,7 +2,10 @@
- if @champ.mandatory?
%span.mandatory *
- if @champ.updated_at.present? && @seen_at.present?
- if @champ.forked_with_changes?
%span.updated-at.highlighted
= @champ.updated_at > 1.minutes.ago ? "modifié à linstant" : "modification à déposer"
- elsif @champ.updated_at.present? && @seen_at.present?
%span.updated-at{ class: highlight_if_unseen_class }
= "modifié le #{try_format_datetime(@champ.updated_at)}"

View file

@ -6,7 +6,7 @@ module TurboChampsConcern
def champs_to_turbo_update(params, champs)
champ_ids = params.keys.map(&:to_i)
to_update = champs.filter { _1.id.in?(champ_ids) && _1.refresh_after_update? }
to_update = champs.filter { _1.id.in?(champ_ids) && (_1.refresh_after_update? || _1.forked_with_changes?) }
to_show, to_hide = champs.filter(&:conditional?)
.partition(&:visible?)
.map { champs_to_one_selector(_1 - to_update) }

View file

@ -6,12 +6,12 @@ module Users
layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret]
ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all]
ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :update_brouillon, :submit_brouillon, :modifier, :update, :create_commentaire, :papertrail, :restore]
ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :update, :create_commentaire, :papertrail, :restore]
before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE
before_action :ensure_ownership_or_invitation!, only: ACTIONS_ALLOWED_TO_OWNER_OR_INVITE
before_action :ensure_dossier_can_be_updated, only: [:update_identite, :update_siret, :brouillon, :update_brouillon, :submit_brouillon, :modifier, :update]
before_action :ensure_dossier_can_be_filled, only: [:brouillon, :modifier, :update_brouillon, :submit_brouillon, :update]
before_action :ensure_dossier_can_be_updated, only: [:update_identite, :update_siret, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :update]
before_action :ensure_dossier_can_be_filled, only: [:brouillon, :modifier, :submit_brouillon, :submit_en_construction, :update]
before_action :ensure_dossier_can_be_viewed, only: [:show]
before_action :forbid_invite_submission!, only: [:submit_brouillon]
before_action :forbid_closed_submission!, only: [:submit_brouillon]
@ -202,21 +202,30 @@ module Users
@dossier = dossier_with_champs
end
def update_brouillon
@dossier = dossier_with_champs
update_dossier_and_compute_errors
def submit_en_construction
@dossier = dossier.find_editing_fork(dossier.user)
@dossier = dossier_with_champs(pj_template: false)
errors = submit_dossier_and_compute_errors
respond_to do |format|
format.html { render :brouillon }
format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all)
if errors.blank?
editing_fork_origin = @dossier.editing_fork_origin
editing_fork_origin.merge_fork(@dossier)
redirect_to dossier_path(editing_fork_origin)
else
flash.now.alert = errors
render(:update, layout: false)
respond_to do |format|
format.html { render :modifier }
format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all)
render :update, layout: false
end
end
end
end
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
@ -225,9 +234,9 @@ module Users
end
respond_to do |format|
format.html { render :modifier }
format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all)
render :update, layout: false
end
end
end
@ -425,8 +434,8 @@ module Users
end
def dossier_scope
if action_name == 'update_brouillon'
Dossier.visible_by_user.or(Dossier.for_procedure_preview)
if action_name == 'update'
Dossier.visible_by_user.or(Dossier.for_procedure_preview).or(Dossier.for_editing_fork)
elsif action_name == 'restore'
Dossier.hidden_by_user
else
@ -488,10 +497,6 @@ module Users
RoutingEngine.compute(@dossier)
end
if dossier.en_construction?
errors += format_errors(errors: @dossier.check_mandatory_and_visible_champs)
end
errors
end
@ -528,9 +533,12 @@ module Users
def append_anchor_link(str_error, model)
return str_error.full_message if !model.is_a?(Champ)
route_helper = @dossier.editing_fork? ? :modifier_dossier_path : :brouillon_dossier_path
[
"Le champ « #{model.libelle.truncate(200)} » #{str_error}",
helpers.link_to(t('views.users.dossiers.fix_champ'), brouillon_dossier_path(anchor: model.input_id))
helpers.link_to(t('views.users.dossiers.fix_champ'), public_send(route_helper, anchor: model.input_id))
].join(", ")
rescue # case of invalid type de champ on champ
str_error

View file

@ -244,6 +244,10 @@ class Champ < ApplicationRecord
input_id
end
def forked_with_changes?
public? && dossier.champ_forked_with_changes?(self)
end
private
def html_id

View file

@ -1,17 +1,13 @@
- dossier_for_editing = dossier.en_construction? ? dossier.owner_editing_fork : dossier
- if dossier.france_connect_information.present?
- content_for(:notice_info) do
= render partial: "shared/dossiers/france_connect_informations_notice", locals: { user_information: dossier.france_connect_information }
.dossier-edit.container.counter-start-header-section
= render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier }
- if dossier.brouillon?
- form_options = { url: brouillon_dossier_url(dossier), method: :patch }
- else
- form_options = { url: modifier_dossier_url(dossier), method: :patch }
= render NestedForms::FormOwnerComponent.new
= form_for dossier, form_options.merge({ html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } }) do |f|
= form_for dossier_for_editing, url: brouillon_dossier_url(dossier), method: :patch, html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } do |f|
%header.mb-6
.fr-highlight
%p.fr-text--sm
@ -42,5 +38,7 @@
= f.select :groupe_instructeur_id,
dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] },
{ include_blank: dossier.brouillon? }
= render EditableChamp::SectionComponent.new(champs: dossier.champs_public)
= render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: false)
= render EditableChamp::SectionComponent.new(champs: dossier_for_editing.champs_public)
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)

View file

@ -4,8 +4,14 @@
= turbo_stream.hide_all(@to_hide)
- @to_update.each do |champ|
= fields_for champ.input_name, champ do |form|
= turbo_stream.replace champ.input_group_id do
= render EditableChamp::EditableChampComponent.new champ:, form:
- if champ.refresh_after_update?
= turbo_stream.replace champ.input_group_id do
= render EditableChamp::EditableChampComponent.new champ:, form:
- else
= turbo_stream.update champ.labelledby_id do
= render EditableChamp::ChampLabelContentComponent.new champ:
= turbo_stream.remove_all(".editable-champ .spinner-removable");
= turbo_stream.hide_all(".editable-champ .spinner");
= turbo_stream.replace_all '.dossier-edit-sticky-footer' do
= render Dossiers::EditFooterComponent.new(dossier: @dossier, annotation: false)

View file

@ -315,10 +315,10 @@ Rails.application.routes.draw do
post 'siret', to: 'dossiers#update_siret'
get 'etablissement'
get 'brouillon'
patch 'brouillon', to: 'dossiers#update_brouillon'
patch 'brouillon', to: 'dossiers#update'
post 'brouillon', to: 'dossiers#submit_brouillon'
get 'modifier', to: 'dossiers#modifier'
patch 'modifier', to: 'dossiers#update'
post 'modifier', to: 'dossiers#submit_en_construction'
get 'merci'
get 'demande'
get 'messagerie'

View file

@ -445,11 +445,76 @@ describe Users::DossiersController, type: :controller do
end
end
describe '#update_brouillon' do
describe '#submit_en_construction' do
before { sign_in(user) }
let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_piece_justificative) }
let!(:dossier) { create(:dossier, user: user, procedure: procedure) }
let!(:dossier) { create(:dossier, :en_construction, user: user) }
let(:first_champ) { dossier.owner_editing_fork.champs_public.first }
let(:anchor_to_first_champ) { controller.helpers.link_to I18n.t('views.users.dossiers.fix_champ'), modifier_dossier_path(anchor: first_champ.input_id) }
let(:value) { 'beautiful value' }
let(:now) { Time.zone.parse('01/01/2100') }
let(:payload) { { id: dossier.id } }
before { dossier.owner_editing_fork }
subject do
Timecop.freeze(now) do
post :submit_en_construction, params: payload
end
end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
it 'redirects to the dossiers list' do
subject
expect(response).to redirect_to(dossier_path(dossier))
expect(flash.alert).to eq('Votre dossier ne peut plus être modifié')
end
end
context 'when the update fails' do
before do
expect_any_instance_of(Dossier).to receive(:valid?).and_return(false)
expect_any_instance_of(Dossier).to receive(:errors).and_return(
[double(class: ActiveModel::Error, full_message: 'nop', base: first_champ)]
)
subject
end
it { expect(response).to render_template(:modifier) }
it { expect(flash.alert).to eq(["Le champ « #{first_champ.libelle} » nop, #{anchor_to_first_champ}"]) }
end
context 'when a mandatory champ is missing' do
let(:value) { nil }
before do
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
subject
end
it { expect(response).to render_template(:modifier) }
it { expect(flash.alert).to eq(["Le champ « l » doit être rempli, #{anchor_to_first_champ}"]) }
end
context 'when dossier has no champ' do
let(:submit_payload) { { id: dossier.id } }
it 'does not raise any errors' do
subject
expect(response).to redirect_to(dossier_path(dossier))
end
end
end
describe '#update brouillon' do
before { sign_in(user) }
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}, { type: :piece_justificative }]) }
let(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:piece_justificative_champ) { dossier.champs_public.last }
let(:value) { 'beautiful value' }
@ -461,16 +526,16 @@ describe Users::DossiersController, type: :controller do
id: dossier.id,
dossier: {
groupe_instructeur_id: dossier.groupe_instructeur_id,
champs_public_attributes: [
{
champs_public_attributes: {
first_champ.id => {
id: first_champ.id,
value: value
},
{
piece_justificative_champ.id => {
id: piece_justificative_champ.id,
piece_justificative_file: file
}
]
}
}
}
end
@ -478,12 +543,12 @@ describe Users::DossiersController, type: :controller do
subject do
Timecop.freeze(now) do
patch :update_brouillon, params: payload
patch :update, params: payload, format: :turbo_stream
end
end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
let(:dossier) { create(:dossier, :en_instruction, user:, procedure:) }
it 'redirects to the dossiers list' do
subject
@ -507,7 +572,7 @@ describe Users::DossiersController, type: :controller do
{
id: dossier.id,
dossier: {
champs_public_attributes: [{ value: '' }]
champs_public_attributes: { first_champ.id => { id: first_champ.id } }
}
}
end
@ -520,7 +585,7 @@ describe Users::DossiersController, type: :controller do
end
context 'when the user has an invitation but is not the owner' do
let(:dossier) { create(:dossier) }
let(:dossier) { create(:dossier, procedure: procedure) }
let!(:invite) { create(:invite, dossier: dossier, user: user) }
before { subject }
@ -530,11 +595,11 @@ describe Users::DossiersController, type: :controller do
end
end
describe '#update' do
describe '#update en_construction' do
before { sign_in(user) }
let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_piece_justificative) }
let!(:dossier) { create(:dossier, :en_construction, user: user, procedure: procedure) }
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}, { type: :piece_justificative }]) }
let!(:dossier) { create(:dossier, :en_construction, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:anchor_to_first_champ) { controller.helpers.link_to I18n.t('views.users.dossiers.fix_champ'), brouillon_dossier_path(anchor: first_champ.input_id) }
let(:piece_justificative_champ) { dossier.champs_public.last }
@ -547,16 +612,16 @@ describe Users::DossiersController, type: :controller do
id: dossier.id,
dossier: {
groupe_instructeur_id: dossier.groupe_instructeur_id,
champs_public_attributes: [
{
champs_public_attributes: {
first_champ.id => {
id: first_champ.id,
value: value
},
{
piece_justificative_champ.id => {
id: piece_justificative_champ.id,
piece_justificative_file: file
}
]
}
}
}
end
@ -564,12 +629,12 @@ describe Users::DossiersController, type: :controller do
subject do
Timecop.freeze(now) do
patch :update, params: payload
patch :update, params: payload, format: :turbo_stream
end
end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
let!(:dossier) { create(:dossier, :en_instruction, user:, procedure:) }
it 'redirects to the dossiers list' do
subject
@ -612,12 +677,12 @@ describe Users::DossiersController, type: :controller do
{
id: dossier.id,
dossier: {
champs_public_attributes: [
{
champs_public_attributes: {
piece_justificative_champ.id => {
id: piece_justificative_champ.id,
piece_justificative_file: file
}
]
}
}
}
end
@ -640,7 +705,7 @@ describe Users::DossiersController, type: :controller do
subject
end
it { expect(response).to render_template(:modifier) }
it { expect(response).to render_template(:update) }
it { expect(flash.alert).to eq(["Le champ « #{first_champ.libelle} » nop, #{anchor_to_first_champ}"]) }
it 'does not update the dossier timestamps' do
@ -656,18 +721,6 @@ describe Users::DossiersController, type: :controller do
end
end
context 'when a mandatory champ is missing' do
let(:value) { nil }
before do
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
subject
end
it { expect(response).to render_template(:modifier) }
it { expect(flash.alert).to eq(["Le champ « l » doit être rempli, #{anchor_to_first_champ}"]) }
end
context 'when a champ validation fails' do
let(:value) { 'abc' }
@ -682,8 +735,8 @@ describe Users::DossiersController, type: :controller do
end
context 'when the user has an invitation but is not the owner' do
let(:dossier) { create(:dossier, :en_construction) }
let!(:invite) { create(:invite, dossier: dossier, user: user) }
let(:dossier) { create(:dossier, :en_construction, procedure:) }
let!(:invite) { create(:invite, dossier:, user:) }
before { subject }
@ -692,9 +745,9 @@ describe Users::DossiersController, type: :controller do
end
context 'when the dossier is followed by an instructeur' do
let(:dossier) { create(:dossier) }
let(:dossier) { create(:dossier, procedure:) }
let(:instructeur) { create(:instructeur) }
let!(:invite) { create(:invite, dossier: dossier, user: user) }
let!(:invite) { create(:invite, dossier:, user:) }
before do
instructeur.follow(dossier)
@ -708,8 +761,8 @@ describe Users::DossiersController, type: :controller do
end
context 'when the champ is a phone number' do
let(:procedure) { create(:procedure, :published, :with_phone) }
let!(:dossier) { create(:dossier, :en_construction, user: user, procedure: procedure) }
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :phone }]) }
let!(:dossier) { create(:dossier, :en_construction, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first }
let(:now) { Time.zone.parse('01/01/2100') }
@ -717,12 +770,12 @@ describe Users::DossiersController, type: :controller do
{
id: dossier.id,
dossier: {
champs_public_attributes: [
{
champs_public_attributes: {
first_champ.id => {
id: first_champ.id,
value: value
}
]
}
}
}
end

View file

@ -119,6 +119,8 @@ describe 'The routing', js: true do
fill_in litteraire_user.dossiers.first.champs_public.first.libelle, with: 'some value'
wait_for_autosave(false)
click_on 'Déposer les modifications'
log_out
# the litteraires instructeurs should have a notification
@ -217,6 +219,8 @@ describe 'The routing', js: true do
expect(page).to have_text(new_group)
click_on 'Déposer les modifications'
log_out
end

View file

@ -146,6 +146,8 @@ describe 'The routing with rules', js: true do
fill_in litteraire_user.dossiers.first.champs_public.first.libelle, with: 'some value'
wait_for_autosave(false)
click_on 'Déposer les modifications'
log_out
# the litteraires instructeurs should have a notification
@ -245,6 +247,8 @@ describe 'The routing with rules', js: true do
expect(page).to have_text(new_group)
click_on 'Déposer les modifications'
log_out
end

View file

@ -483,13 +483,13 @@ describe 'The user' do
fill_individual
# Test autosave failure
allow_any_instance_of(Users::DossiersController).to receive(:update_brouillon).and_raise("Server is busy")
allow_any_instance_of(Users::DossiersController).to receive(:update).and_raise("Server is busy")
fill_in('texte obligatoire', with: 'a valid user input')
blur
expect(page).to have_css('span', text: 'Impossible denregistrer le brouillon', visible: true)
# Test that retrying after a failure works
allow_any_instance_of(Users::DossiersController).to receive(:update_brouillon).and_call_original
allow_any_instance_of(Users::DossiersController).to receive(:update).and_call_original
click_on 'réessayer'
wait_for_autosave

View file

@ -12,6 +12,7 @@ RSpec.shared_examples 'the user can edit the submitted demande' do
fill_in('Texte obligatoire', with: 'Nouveau texte')
wait_for_autosave(false)
click_on 'Déposer les modifications'
click_on 'Demande'
expect(page).to have_current_path(demande_dossier_path(dossier))

View file

@ -8,7 +8,7 @@ describe "Dossier en_construction" do
}
let(:champ) {
dossier.champs_public.find { _1.type_de_champ_id == tdc.id }
dossier.find_editing_fork(dossier.user).champs_public.find { _1.type_de_champ_id == tdc.id }
}
scenario 'delete a non mandatory piece justificative', js: true do
@ -28,7 +28,7 @@ describe "Dossier en_construction" do
scenario 'remplace a mandatory piece justificative', js: true do
visit_dossier(dossier)
click_on "Remplacer le fichier toto.txt"
click_on "Supprimer le fichier toto.txt"
input_selector = "#attachment-multiple-empty-#{champ.id}"
expect(page).to have_selector(input_selector)
@ -51,9 +51,9 @@ describe "Dossier en_construction" do
scenario 'remplace a mandatory titre identite', js: true do
visit_dossier(dossier)
click_on "Remplacer le fichier toto.png"
click_on "Supprimer le fichier toto.png"
input_selector = ".attachment-input-#{champ.piece_justificative_file.attachments.first.id}"
input_selector = "##{champ.input_id}"
expect(page).to have_selector(input_selector)
find(input_selector).attach_file(Rails.root.join('spec/fixtures/files/file.pdf'))

View file

@ -115,8 +115,8 @@ describe 'shared/dossiers/edit', type: :view do
let(:dossier) { create(:dossier, :en_construction) }
before { dossier.champs_public << champ }
it 'cannot delete a piece justificative' do
expect(subject).not_to have_selector("[title='Supprimer le fichier #{champ.piece_justificative_file.attachments[0].filename}']")
it 'can delete a piece justificative' do
expect(subject).to have_selector("[title='Supprimer le fichier #{champ.piece_justificative_file.attachments[0].filename}']")
end
end