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,24 +1,27 @@
@import "colors"; @import "colors";
@import "constants"; @import "constants";
.dossier-edit { .dossier-header {
.dossier-header { .container {
background-color: $light-grey; padding-bottom: $default-padding;
margin-bottom: $default-padding; }
.container { h1 {
padding: $default-padding; font-size: 22px;
}
h1 { .icon.folder {
font-size: 22px; vertical-align: -3px;
.icon.folder {
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
render 'users/recapitulatif/show' if @facade.dossier.brouillon?
redirect_to modifier_dossier_path(@facade.dossier)
else
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,11 +1,12 @@
- if flash.any? #flash_messages
#flash_message.center - if flash.any?
- flash.each do |key, value| #flash_message.center
- if value.class == Array - flash.each do |key, value|
.alert{ class: flash_class(key) } - if value.class == Array
- value.each do |message| .alert{ class: flash_class(key) }
= sanitize(message) - value.each do |message|
%br = sanitize(message)
- else %br
.alert{ class: flash_class(key) } - else
= sanitize(value) .alert{ class: flash_class(key) }
= sanitize(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
it { expect(response.status).to eq 200 }
end end
end end
context 'when email is set' do context 'when email is blank' do
before do let(:email) { '' }
get :show, params: { id: invite.id, email: email } it { is_expected.to redirect_to new_user_session_path }
end end
context 'when email is blank' do
let(:email) { '' }
context 'when email is not blank' do
context 'when email is affected at an user' do
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 blank' do context 'when email is not affected at an user' do
context 'when email is affected at an user' do let(:email) { 'new_user@octo.com' }
let(:email) { user.email } it { is_expected.to redirect_to new_user_registration_path(user_email: email) }
it { is_expected.to redirect_to new_user_session_path }
end
context 'when email is not affected at an user' do
let(:email) { 'new_user@octo.com' }
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
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 end
context 'when invitation ID is not attach at the user email account' do 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 }