Merge pull request #2275 from betagouv/invite-on-brouillon

Permet à un usager d'inviter quelqu'un d'autre dès le brouillon
This commit is contained in:
gregoirenovel 2018-08-01 18:21:37 +02:00 committed by GitHub
commit f4dc782123
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 387 additions and 86 deletions

View file

@ -10,11 +10,11 @@
line-height: 20px; line-height: 20px;
background-color: #FFFFFF; background-color: #FFFFFF;
color: $black; color: $black;
cursor: pointer;
text-align: center; text-align: center;
-webkit-appearance: none; -webkit-appearance: none;
&:hover { &:hover:not(:disabled) {
cursor: pointer;
background: $light-grey; background: $light-grey;
text-decoration: none; text-decoration: none;
} }
@ -24,12 +24,17 @@
outline: none; outline: none;
} }
&:disabled {
opacity: 0.5;
filter: saturate(50%);
}
&.primary { &.primary {
color: #FFFFFF; color: #FFFFFF;
border-color: $blue; border-color: $blue;
background-color: $blue; background-color: $blue;
&:hover { &:hover:not(:disabled) {
background: $light-blue; background: $light-blue;
} }
} }
@ -39,7 +44,7 @@
border-color: $blue; border-color: $blue;
background-color: #FFFFFF; background-color: #FFFFFF;
&:hover { &:hover:not(:disabled) {
color: #FFFFFF; color: #FFFFFF;
background: $light-blue; background: $light-blue;
} }
@ -62,7 +67,7 @@
border-color: $green; border-color: $green;
background-color: $green; background-color: $green;
&:hover { &:hover:not(:disabled) {
color: $green; color: $green;
background-color: #FFFFFF; background-color: #FFFFFF;
} }
@ -73,7 +78,7 @@
border-color: $black; border-color: $black;
background-color: $black; background-color: $black;
&:hover { &:hover:not(:disabled) {
color: $black; color: $black;
background-color: #FFFFFF; background-color: #FFFFFF;
} }
@ -84,7 +89,7 @@
border-color: $dark-red; border-color: $dark-red;
background-color: $dark-red; background-color: $dark-red;
&:hover { &:hover:not(:disabled) {
color: $dark-red; color: $dark-red;
background-color: #FFFFFF; background-color: #FFFFFF;
} }

View file

@ -1,13 +1,9 @@
@import "colors"; @import "colors";
@import "constants"; @import "constants";
.dossier-edit {
.dossier-header { .dossier-header {
background-color: $light-grey;
margin-bottom: $default-padding;
.container { .container {
padding: $default-padding; padding-bottom: $default-padding;
} }
h1 { h1 {
@ -17,8 +13,15 @@
vertical-align: -3px; vertical-align: -3px;
} }
} }
.dossier-form-actions {
margin-top: $default-padding;
margin-bottom: $default-padding;
text-align: right;
}
} }
.dossier-edit {
.prologue { .prologue {
margin: (1.5 * $default-padding) 0; margin: (1.5 * $default-padding) 0;
display: flex; display: flex;

View file

@ -316,6 +316,15 @@
} }
} }
.send-notice {
@include notice-text-style;
margin-bottom: $default-padding;
}
.send-wrapper + .send-notice {
margin-top: - $default-padding;
}
.inline-champ { .inline-champ {
margin-left: $default-spacer; margin-left: $default-spacer;
margin-right: $default-spacer; margin-right: $default-spacer;

View file

@ -0,0 +1,35 @@
@import "constants";
#invites-form {
padding: $default-padding;
text-align: left;
form {
display: flex;
margin-top: $default-padding;
}
h4 {
font-weight: bold;
margin-bottom: $default-spacer;
}
p {
margin-bottom: $default-spacer;
}
ul {
list-style-position: inside;
list-style-type: disc;
margin-bottom: $default-padding;
}
input[type=email] {
width: auto;
margin-bottom: 0;
}
.button {
margin-left: $default-spacer;
}
}

View file

@ -2,10 +2,11 @@ class InvitesController < ApplicationController
before_action :ensure_user_signed_in before_action :ensure_user_signed_in
def create def create
email = params[:email].downcase email = params[:invite_email].downcase
dossier = current_user.dossiers.find(params[:dossier_id])
invite = InviteUser.create( invite = InviteUser.create(
dossier: current_user.dossiers.find(params[:dossier_id]), dossier: dossier,
user: User.find_by(email: email), user: User.find_by(email: email),
email: email, email: email,
email_sender: current_user.email email_sender: current_user.email
@ -18,12 +19,15 @@ class InvitesController < ApplicationController
InviteMailer.invite_guest(invite).deliver_later InviteMailer.invite_guest(invite).deliver_later
end end
flash.notice = "Invitation envoyée (#{invite.email})" flash.notice = "Une invitation a été envoyée à #{invite.email}."
else else
flash.alert = invite.errors.full_messages flash.alert = invite.errors.full_messages
end end
redirect_to url_for(controller: 'users/recapitulatif', action: :show, dossier_id: params['dossier_id']) respond_to do |format|
format.html { redirect_back(fallback_location: helpers.url_for_dossier(dossier)) }
format.js { @dossier = dossier }
end
end end
private private

View file

@ -9,7 +9,11 @@ class Users::Dossiers::InvitesController < UsersController
def show def show
@facade = InviteDossierFacades.new params[:id].to_i, current_user.email @facade = InviteDossierFacades.new params[:id].to_i, current_user.email
if @facade.dossier.brouillon?
redirect_to modifier_dossier_path(@facade.dossier)
else
render 'users/recapitulatif/show' render 'users/recapitulatif/show'
end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
flash.alert = t('errors.messages.dossier_not_found') flash.alert = t('errors.messages.dossier_not_found')
redirect_to url_for dossiers_path redirect_to url_for dossiers_path

View file

@ -54,6 +54,6 @@ class DossierFacades
end end
def followers def followers
Gestionnaire.joins(:follows).where("follows.dossier_id=#{@dossier.id}") @dossier.followers_gestionnaires
end end
end end

View file

@ -0,0 +1,17 @@
#invites-form
- if dossier.invites.present?
%h4 Personnes invitées à participer à ce dossier
%ul
- dossier.invites.each do |invite|
%li= invite.email
%p Ces personnes peuvent modifier ce dossier.
- if dossier.brouillon?
%p Une fois le dossier complet, vous devez le soumettre vous-même.
- else
%p Vous pouvez inviter quelquun à remplir ce dossier avec vous.
%p Cette personne aura le droit de modifier votre dossier.
= form_tag invites_dossier_path(dossier_id: dossier.id), remote: true, method: :post, class: 'form' do
= email_field_tag :invite_email, '', class: 'small', placeholder: 'adresse email', required: true
= submit_tag 'Envoyer une invitation', class: 'button accepted'

View file

@ -0,0 +1,6 @@
var formView = "<%= escape_javascript(render partial: 'invites/form', locals: { dossier: @dossier }) %>";
document.querySelector("#invites-form").outerHTML = formView;
var flashMessagesView = "<%= escape_javascript(render partial: 'layouts/flash_messages') %>";
document.querySelector("#flash_messages").outerHTML = flashMessagesView;
<% flash.clear %>

View file

@ -1,3 +1,4 @@
#flash_messages
- if flash.any? - if flash.any?
#flash_message.center #flash_message.center
- flash.each do |key, value| - flash.each do |key, value|

View file

@ -20,5 +20,5 @@
%li %li
= form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline', id: 'send-invitation' do = form_tag invites_dossier_path(dossier_id: @facade.dossier.id), method: :post, class: 'form-inline', id: 'send-invitation' do
= text_field_tag :email, '', class: 'form-control', placeholder: 'Envoyer une invitation', id: 'invitation-email' = text_field_tag :invite_email, '', class: 'form-control', placeholder: 'Envoyer une invitation', id: 'invite_email'
= submit_tag 'Ajouter', class: 'btn btn-success', data: { confirm: "Envoyer l'invitation ?" } = submit_tag 'Ajouter', class: 'btn btn-success', data: { confirm: "Envoyer l'invitation ?" }

View file

@ -2,10 +2,8 @@
= render partial: "new_user/dossiers/footer", locals: { dossier: @dossier } = render partial: "new_user/dossiers/footer", locals: { dossier: @dossier }
.dossier-edit .dossier-edit
.dossier-header .dossier-header.sub-header
.container .container
%h1 = render partial: "shared/dossiers/header", locals: { dossier: @dossier, apercu: false }
%span.icon.folder
= @dossier.procedure.libelle
= render partial: "shared/dossiers/edit", locals: { dossier: @dossier, apercu: false } = render partial: "shared/dossiers/edit", locals: { dossier: @dossier, apercu: false }

View file

@ -58,6 +58,15 @@
= link_to "#", class: "button icon-only" do = link_to "#", class: "button icon-only" do
%span.icon.follow %span.icon.follow
%p
%button.button{ disabled: true } .button.disabled
%button.button.primary{ disabled: true } .button.primary.disabled
%button.button.secondary{ disabled: true } .button.secondary.disabled
%button.button.danger{ disabled: true } .button.danger.disabled
%p %p
= link_to ".button.accepted", "#", class: "button accepted" = link_to ".button.accepted", "#", class: "button accepted"

View file

@ -74,9 +74,10 @@
class: 'button send secondary', class: 'button send secondary',
data: { action: 'draft', disable_with: 'Envoi...' } data: { action: 'draft', disable_with: 'Envoi...' }
- if current_user.owns?(dossier) && dossier.can_transition_to_en_construction? - if dossier.can_transition_to_en_construction?
= f.button 'Soumettre le dossier', = f.button 'Soumettre le dossier',
class: 'button send primary', class: 'button send primary',
disabled: !current_user.owns?(dossier),
data: { action: 'submit', disable_with: 'Envoi...' } data: { action: 'submit', disable_with: 'Envoi...' }
- else - else
@ -84,4 +85,8 @@
class: 'button send primary', class: 'button send primary',
data: { action: 'submit', disable_with: 'Envoi...' } data: { action: 'submit', disable_with: 'Envoi...' }
- if dossier.brouillon? && !current_user.owns?(dossier)
.send-notice.invite-cannot-submit
En tant quinvité, vous pouvez remplir ce formulaire mais le titulaire du dossier doit le soumettre lui-même.
= render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier } = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier }

View file

@ -0,0 +1,16 @@
%h1
%span.icon.folder
= dossier.procedure.libelle
.dossier-form-actions
- if current_user.owns?(dossier)
%span.button.dropdown.invite-user-action
%span.icon.person
- if dossier.invites.count > 0
Voir les personnes invitées
%span.badge= dossier.invites.count
- else
Inviter une personne à modifier ce dossier
.dropdown-content.fade-in-down
= render partial: "invites/form", locals: { dossier: dossier }

View file

@ -11,7 +11,7 @@ describe InvitesController, type: :controller do
sign_in signed_in_profile sign_in signed_in_profile
end end
subject { post :create, params: { dossier_id: dossier.id, email: email } } subject { post :create, params: { dossier_id: dossier.id, invite_email: email } }
context "when gestionnaire is signed_in" do context "when gestionnaire is signed_in" do
let(:signed_in_profile) { create(:gestionnaire) } let(:signed_in_profile) { create(:gestionnaire) }
@ -69,11 +69,16 @@ describe InvitesController, type: :controller do
context 'when user has access to dossier' do context 'when user has access to dossier' do
before do before do
request.env["HTTP_REFERER"] = "/dossiers/#{dossier.id}/modifier"
dossier.update(user: signed_in_profile) dossier.update(user: signed_in_profile)
end end
it { expect { subject }.to change(InviteUser, :count).by(1) } it { expect { subject }.to change(InviteUser, :count).by(1) }
it "redirects to the previous URL" do
expect(subject).to redirect_to("/dossiers/#{dossier.id}/modifier")
end
context 'when email is assign to an user' do context 'when email is assign to an user' do
let! (:user_invite) { create(:user, email: email) } let! (:user_invite) { create(:user, email: email) }

View file

@ -96,6 +96,47 @@ describe NewUser::DossiersController, type: :controller do
end end
end end
describe "#forbid_invite_submission!" do
let(:user) { create(:user) }
let(:asked_dossier) { create(:dossier) }
let(:ensure_authorized) { :forbid_invite_submission! }
let(:submit_action) { 'submit' }
before do
@controller.params = @controller.params.merge(dossier_id: asked_dossier.id, submit_action: submit_action)
allow(@controller).to receive(:current_user).and_return(user)
allow(@controller).to receive(:redirect_to)
end
context 'when a user save their own draft' do
let(:asked_dossier) { create(:dossier, user: user) }
let(:submit_action) { 'draft' }
it_behaves_like 'does not redirect nor flash'
end
context 'when a user submit their own dossier' do
let(:asked_dossier) { create(:dossier, user: user) }
let(:submit_action) { 'submit' }
it_behaves_like 'does not redirect nor flash'
end
context 'when an invite save the draft for a dossier where they where invited' do
before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') }
let(:submit_action) { 'draft' }
it_behaves_like 'does not redirect nor flash'
end
context 'when an invite submit a dossier where they where invited' do
before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') }
let(:submit_action) { 'submit' }
it_behaves_like 'redirects and flashes'
end
end
describe 'attestation' do describe 'attestation' do
before { sign_in(user) } before { sign_in(user) }

View file

@ -1,78 +1,78 @@
describe Users::Dossiers::InvitesController, type: :controller do describe Users::Dossiers::InvitesController, type: :controller do
describe '#authenticate_user!' do describe '#authenticate_user!' do
let(:user) { create :user } let(:user) { create :user }
let(:invite) { create :invite } let(:dossier) { create(:dossier, :en_construction) }
let(:invite) { create(:invite, dossier: dossier) }
subject { get :show, params: { id: invite.id, email: email } }
context 'when email is not set' do context 'when email is not set' do
context 'when user is not connected' do let(:email) { nil }
before do
get :show, params: { id: invite.id }
end
context 'and user is not connected' do
it { is_expected.to redirect_to new_user_session_path } it { is_expected.to redirect_to new_user_session_path }
end end
context 'when user is connected' do context 'and user is connected' do
let!(:invite) { create :invite, user: user } let(:invite) { create :invite, dossier: dossier, user: user }
before { sign_in invite.user }
before do it { is_expected.to have_http_status(:ok) }
sign_in invite.user
get :show, params: { id: invite.id }
end end
it { expect(response.status).to eq 200 }
end
end
context 'when email is set' do
before do
get :show, params: { id: invite.id, email: email }
end end
context 'when email is blank' do context 'when email is blank' do
let(:email) { '' } let(:email) { '' }
it { is_expected.to redirect_to new_user_session_path } it { is_expected.to redirect_to new_user_session_path }
end end
context 'when email is not blank' do context 'when email is not blank' do
context 'when email is affected at an user' do context 'when email is affected at an user' do
let(:email) { user.email } let(:email) { user.email }
it { is_expected.to redirect_to new_user_session_path } it { is_expected.to redirect_to new_user_session_path }
end end
context 'when email is not affected at an user' do context 'when email is not affected at an user' do
let(:email) { 'new_user@octo.com' } let(:email) { 'new_user@octo.com' }
it { is_expected.to redirect_to new_user_registration_path(user_email: email) } it { is_expected.to redirect_to new_user_registration_path(user_email: email) }
end end
end end
end end
end
describe '#GET show' do describe '#GET show' do
let(:user) { create :user } let(:user) { create :user }
let(:dossier) { create :dossier }
let(:invite) { create :invite, email: email, dossier: (create :dossier) } let(:invite) { create :invite, email: email, dossier: dossier }
subject { get :show, params: { id: invite.id } }
before do before do
sign_in user sign_in user
end end
context 'when invitation ID is attach at the user email account' do subject! { get :show, params: { id: invite.id } }
context 'when invitation ID is attached at the user email account' do
let(:email) { user.email } let(:email) { user.email }
it { expect(subject.status).to eq 200 }
context 'and dossier is a brouillon' do
let(:dossier) { create :dossier, state: 'brouillon' }
it { is_expected.to have_http_status(302) }
it { is_expected.to redirect_to modifier_dossier_path(dossier) }
end end
context 'when invitation ID is not attach at the user email account' do context 'and dossier is not a brouillon' do
let(:dossier) { create :dossier, :en_construction }
it { is_expected.to have_http_status(:ok) }
it { is_expected.to render_template('users/recapitulatif/show') }
end
end
context 'when invitation ID is not attached at the user email account' do
let(:email) { 'fake@email.com' } let(:email) { 'fake@email.com' }
it { expect(subject.status).to eq 302 } it { is_expected.to have_http_status(302) }
it { is_expected.to redirect_to dossiers_path } it { is_expected.to redirect_to dossiers_path }
it { expect(flash[:alert]).to be_present }
end end
end end
end end

View file

@ -0,0 +1,135 @@
require 'spec_helper'
feature 'Invitations' do
let(:user) { create(:user) }
let(:invited_user) { create(:user, email: 'user_invite@exemple.fr') }
let(:procedure) { create(:procedure, :published, :with_type_de_champ) }
let(:invite) { create(:invite_user, user: invited_user, dossier: dossier) }
context 'when the dossier is a brouillon' do
let!(:dossier) { create(:dossier, :for_individual, state: 'brouillon', user: user, procedure: procedure) }
scenario 'on the form, a user can invite another user to collaborate on the dossier', js: true do
log_in(user)
navigate_to_brouillon(dossier)
fill_in 'Libelle du champ', with: 'Some edited value'
send_invite_to "user_invite@exemple.fr"
expect(page).to have_current_path(modifier_dossier_path(dossier))
expect(page).to have_text("Une invitation a été envoyée à user_invite@exemple.fr.")
expect(page).to have_text("user_invite@exemple.fr")
# Ensure unsaved edits to the form are not lost
expect(page).to have_field('Libelle du champ', with: 'Some edited value')
end
scenario 'an invited user can see and edit the draft', js: true do
visit users_dossiers_invite_path(invite)
expect(page).to have_current_path(new_user_session_path)
submit_login_form(invited_user)
expect(page).to have_current_path(modifier_dossier_path(dossier))
expect(page).to have_no_selector('.button.invite-user-action')
fill_in 'Libelle du champ', with: 'Some edited value'
click_button 'Enregistrer le brouillon'
expect(page).to have_text('Votre brouillon a bien été sauvegardé')
expect(page).to have_field('Libelle du champ', with: 'Some edited value')
end
scenario 'an invited user cannot submit the draft' do
visit users_dossiers_invite_path(invite)
expect(page).to have_current_path(new_user_session_path)
submit_login_form(invited_user)
expect(page).to have_current_path(modifier_dossier_path(dossier))
expect(page).to have_button('Soumettre le dossier', disabled: true)
expect(page).to have_selector('.invite-cannot-submit')
end
end
context 'when the dossier is en_construction' do
let!(:dossier) { create(:dossier, :for_individual, :en_construction, user: user, procedure: procedure) }
scenario 'on dossier details, a user can invite another user to collaborate on the dossier', js: true do
log_in(user)
navigate_to_recapitulatif(dossier)
legacy_send_invite_to "user_invite@exemple.fr"
expect(page).to have_current_path(users_dossier_recapitulatif_path(dossier))
expect(page).to have_text("Une invitation a été envoyée à user_invite@exemple.fr.")
expect(page).to have_text("user_invite@exemple.fr")
end
scenario 'an invited user can see and edit the dossier', js: true do
visit users_dossiers_invite_path(invite)
expect(page).to have_current_path(new_user_session_path)
submit_login_form(invited_user)
expect(page).to have_current_path(users_dossiers_invite_path(invite))
expect(page).to have_no_selector('.button.invite-user-action')
expect(page).to have_text("Dossier nº #{dossier.id}")
# We should be able to just click() the link, but Capybara detects that the
# enclosing div would be clicked instead.
expect(page).to have_link("MODIFIER", href: modifier_dossier_path(dossier))
visit modifier_dossier_path(dossier)
expect(page).to have_current_path(modifier_dossier_path(dossier))
fill_in "Libelle du champ", with: "Some edited value"
click_button "Enregistrer les modifications du dossier"
expect(page).to have_current_path(users_dossiers_invite_path(invite))
expect(page).to have_text("Some edited value")
end
end
private
def log_in(user)
visit '/'
click_on 'Connexion'
submit_login_form(user)
expect(page).to have_current_path(dossiers_path)
end
def submit_login_form(user)
fill_in 'user_email', with: user.email
fill_in 'user_password', with: user.password
click_on 'Se connecter'
end
def navigate_to_brouillon(dossier)
expect(page).to have_current_path(dossiers_path)
click_on(dossier.id)
expect(page).to have_current_path(modifier_dossier_path(dossier))
end
def navigate_to_recapitulatif(dossier)
expect(page).to have_current_path(dossiers_path)
click_on(dossier.id)
expect(page).to have_current_path(users_dossier_recapitulatif_path(dossier))
end
def send_invite_to(invited_email)
find('.button.invite-user-action').click()
expect(page).to have_button("Envoyer une invitation", visible: true)
fill_in 'invite_email', with: invited_email
click_on "Envoyer une invitation"
end
def legacy_send_invite_to(invited_email)
find('.dropdown-toggle', text: "Voir les personnes impliquées").click()
expect(page).to have_button("Ajouter", visible: true)
fill_in 'invite_email', with: invited_email
page.accept_alert "Envoyer l'invitation ?" do
click_on "Ajouter"
end
end
end

View file

@ -189,6 +189,14 @@ describe Dossier do
let(:dossier) { create(:dossier, :with_entreprise, user: user, procedure: procedure, en_construction_at: date1, en_instruction_at: date2, processed_at: date3, motivation: "Motivation") } let(:dossier) { create(:dossier, :with_entreprise, user: user, procedure: procedure, en_construction_at: date1, en_instruction_at: date2, processed_at: date3, motivation: "Motivation") }
let!(:follow) { create(:follow, gestionnaire: gestionnaire, dossier: dossier) } let!(:follow) { create(:follow, gestionnaire: gestionnaire, dossier: dossier) }
describe "followers_gestionnaires" do
let(:non_following_gestionnaire) { create(:gestionnaire) }
subject { dossier.followers_gestionnaires }
it { expect(subject).to eq [gestionnaire] }
it { expect(subject).not_to include(non_following_gestionnaire) }
end
describe '#export_headers' do describe '#export_headers' do
subject { dossier.export_headers } subject { dossier.export_headers }