Merge pull request #6123 from betagouv/main

2021-04-22-01
This commit is contained in:
LeSim 2021-04-22 15:02:02 +02:00 committed by GitHub
commit 396299cd8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 458 additions and 366 deletions

View file

@ -49,7 +49,7 @@ gem 'kaminari', '1.2.1' # Pagination
gem 'listen' # Required by ActiveSupport::EventedFileUpdateChecker gem 'listen' # Required by ActiveSupport::EventedFileUpdateChecker
gem 'lograge' gem 'lograge'
gem 'logstash-event' gem 'logstash-event'
gem 'mailjet' gem 'mailjet', require: false
gem 'openid_connect' gem 'openid_connect'
gem 'pg' gem 'pg'
gem 'phonelib' gem 'phonelib'

View file

@ -4,3 +4,9 @@
font-family: "Muli", system-ui, -apple-system, sans-serif; font-family: "Muli", system-ui, -apple-system, sans-serif;
color: $black; color: $black;
} }
ol {
line-height: 28px;
list-style-type: decimal;
list-style-position: inside;
}

View file

@ -10,7 +10,7 @@
// The procedure description can still be read from the /commencer // The procedure description can still be read from the /commencer
// pages. // pages.
@media (max-width: $two-columns-breakpoint) { @media (max-width: $two-columns-breakpoint) {
.procedure-description { .procedure-preview {
display: none; display: none;
} }
} }

View file

@ -45,3 +45,7 @@
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
.flex-no-shrink {
flex-shrink: 0;
}

View file

@ -5,26 +5,38 @@
padding-top: 2 * $default-padding; padding-top: 2 * $default-padding;
padding-bottom: 2 * $default-padding; padding-bottom: 2 * $default-padding;
text-align: center; text-align: center;
max-width: 600px; max-width: 700px;
b { section {
text-align: left;
margin: 3 * $default-padding auto;
}
p,
ol {
margin-top: $default-padding;
margin-bottom: $default-padding;
}
.link-sent-info {
color: #000000;
background-color: $yellow;
padding: 0 $default-padding;
border: 1px solid transparent; // prevent margin collapse of first paragraph
}
.link-sent-help {
border-top: 1px solid $grey;
padding-top: $default-padding;
margin-bottom: $default-padding;
}
.link-sent-help-title {
font-weight: bold; font-weight: bold;
} }
p { .link-sent-help-list {
text-align: left; list-style-position: outside;
margin: 6 * $default-spacer auto; padding-left: $default-padding;
}
p.mail {
color: #000000;
background-color: $yellow;
padding: $default-padding;
}
p.help {
border-top: 1px solid $grey;
padding-top: 6 * $default-spacer;
margin-bottom: 2 * $default-spacer;
} }
} }

View file

@ -36,3 +36,13 @@
font-size: 20px; font-size: 20px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-metadata {
margin-top: -8px;
margin-bottom: 8px;
li {
font-size: 14px;
}
}

View file

@ -108,10 +108,10 @@ module Instructeurs
@dossiers = @dossiers.where(id: filtered_sorted_paginated_ids) @dossiers = @dossiers.where(id: filtered_sorted_paginated_ids)
@dossiers = procedure_presentation.eager_load_displayed_fields(@dossiers)
@dossiers = @dossiers.sort_by { |d| filtered_sorted_paginated_ids.index(d.id) } @dossiers = @dossiers.sort_by { |d| filtered_sorted_paginated_ids.index(d.id) }
@projected_dossiers = DossierProjectionService.project(filtered_sorted_paginated_ids, procedure_presentation.displayed_fields)
kaminarize(page, filtered_sorted_ids.count) kaminarize(page, filtered_sorted_ids.count)
assign_exports assign_exports

View file

@ -33,6 +33,10 @@ class Users::PasswordsController < Devise::PasswordsController
# super # super
# end # end
def reset_link_sent
@email = params[:email]
end
# protected # protected
# def after_resetting_password_path_for(resource) # def after_resetting_password_path_for(resource)
@ -74,4 +78,9 @@ class Users::PasswordsController < Devise::PasswordsController
def password_params def password_params
params.require(:user).permit(:reset_password_token, :password) params.require(:user).permit(:reset_password_token, :password)
end end
def after_sending_reset_password_instructions_path_for(resource_name)
flash.discard(:notice)
users_password_reset_link_sent_path(email: resource.email)
end
end end

View file

@ -19,6 +19,13 @@ function buildURL(scope, term) {
return `${api_adresse_url}/search?q=${term}&limit=5`; return `${api_adresse_url}/search?q=${term}&limit=5`;
} else if (scope === 'annuaire-education') { } else if (scope === 'annuaire-education') {
return `${api_education_url}/search?dataset=fr-en-annuaire-education&q=${term}&rows=5`; return `${api_education_url}/search?dataset=fr-en-annuaire-education&q=${term}&rows=5`;
} else if (scope === 'communes') {
if (isNumeric(term)) {
return `${api_geo_url}/communes?codePostal=${term}&limit=5`;
}
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
const limit = term.length > 5 ? 10 : 5;
return `${api_geo_url}/communes?nom=${term}&boost=population&limit=${limit}`;
} else if (isNumeric(term)) { } else if (isNumeric(term)) {
const code = term.padStart(2, '0'); const code = term.padStart(2, '0');
return `${api_geo_url}/${scope}?code=${code}&limit=5`; return `${api_geo_url}/${scope}?code=${code}&limit=5`;

View file

@ -1,6 +1,7 @@
# Preview all emails at http://localhost:3000/rails/mailers/devise_user_mailer # Preview all emails at http://localhost:3000/rails/mailers/devise_user_mailer
class DeviseUserMailer < Devise::Mailer class DeviseUserMailer < Devise::Mailer
helper :application # gives access to all helpers defined within `application_helper`. helper :application # gives access to all helpers defined within `application_helper`.
helper MailerHelper
include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url` include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url`
layout 'mailers/layout' layout 'mailers/layout'

View file

@ -90,10 +90,6 @@ class ProcedurePresentation < ApplicationRecord
] ]
end end
def displayed_fields_values(dossier)
displayed_fields.map { |field| get_value(dossier, field[TABLE], field[COLUMN]) }
end
def sorted_ids(dossiers, instructeur) def sorted_ids(dossiers, instructeur)
table, column, order = sort.values_at(TABLE, COLUMN, 'order') table, column, order = sort.values_at(TABLE, COLUMN, 'order')
@ -178,25 +174,6 @@ class ProcedurePresentation < ApplicationRecord
end.reduce(:&) end.reduce(:&)
end end
def eager_load_displayed_fields(dossiers)
relations_to_include = displayed_fields
.pluck(TABLE)
.reject { |table| table == 'self' }
.map do |table|
case table
when TYPE_DE_CHAMP
{ champs: :type_de_champ }
when TYPE_DE_CHAMP_PRIVATE
{ champs_private: :type_de_champ }
else
table
end
end
.uniq
dossiers.includes(relations_to_include)
end
def human_value_for_filter(filter) def human_value_for_filter(filter)
case filter[TABLE] case filter[TABLE]
when TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE when TYPE_DE_CHAMP, TYPE_DE_CHAMP_PRIVATE
@ -314,23 +291,6 @@ class ProcedurePresentation < ApplicationRecord
end end
end end
def get_value(dossier, table, column)
case table
when 'self'
dossier.send(column)&.strftime('%d/%m/%Y')
when 'user', 'individual', 'etablissement'
dossier.send(table)&.send(column)
when 'followers_instructeurs'
dossier.send(table)&.map { |g| g.send(column) }&.join(', ')
when TYPE_DE_CHAMP
dossier.champs.find { |c| c.stable_id == column.to_i }.to_s
when TYPE_DE_CHAMP_PRIVATE
dossier.champs_private.find { |c| c.stable_id == column.to_i }.to_s
when 'groupe_instructeur'
dossier.groupe_instructeur.label
end
end
def field_hash(label, table, column) def field_hash(label, table, column)
{ {
'label' => label, 'label' => label,

View file

@ -0,0 +1,88 @@
class DossierProjectionService
class DossierProjection < Struct.new(:dossier, :columns)
end
TABLE = 'table'
COLUMN = 'column'
# Returns [DossierProjection(dossier, columns)] ordered by dossiers_ids
# and the columns orderd by fields.
#
# It tries to be fast by using `pluck` (or at least `select`)
# to avoid deserializing entire records.
#
# It stores its intermediary queries results in an hash in the corresponding field.
# ex: field_email[:id_value_h] = { dossier_id_1: email_1, dossier_id_3: email_3 }
#
# Those hashes are needed because:
# - the order of the intermediary query results are unknown
# - some values can be missing (if a revision added or removed them)
def self.project(dossiers_ids, fields)
champ_fields, other_fields = fields
.partition { |f| ['type_de_champ', 'type_de_champ_private'].include?(f[TABLE]) }
if champ_fields.present?
Champ
.includes(:type_de_champ)
.where(
# as querying the champs table is costly
# we fetch all the requested champs at once
types_de_champ: { stable_id: champ_fields.map { |f| f[COLUMN] } },
dossier_id: dossiers_ids
)
.select(:dossier_id, :value, :type_de_champ_id, :stable_id) # we cannot pluck :value, as we need the champ.to_s method
.group_by(&:stable_id) # the champs are redispatched to their respective fields
.map do |stable_id, champs|
field = champ_fields.find { |f| f[COLUMN] == stable_id.to_s }
field[:id_value_h] = champs.to_h { |c| [c.dossier_id, c.to_s] }
end
end
other_fields.each do |field|
field[:id_value_h] = case field[TABLE]
when 'self'
Dossier
.where(id: dossiers_ids)
.pluck(:id, field[COLUMN].to_sym)
.to_h { |id, col| [id, col&.strftime('%d/%m/%Y')] }
when 'user'
Dossier
.joins(:user)
.where(id: dossiers_ids)
.pluck('dossiers.id, users.email')
.to_h
when 'individual'
Individual
.where(dossier_id: dossiers_ids)
.pluck(:dossier_id, field[COLUMN].to_sym)
.to_h
when 'etablissement'
Etablissement
.where(dossier_id: dossiers_ids)
.pluck(:dossier_id, field[COLUMN].to_sym)
.to_h
when 'groupe_instructeur'
Dossier
.joins(:groupe_instructeur)
.where(id: dossiers_ids)
.pluck('dossiers.id, groupe_instructeurs.label')
.to_h
when 'followers_instructeurs'
Follow
.active
.joins(instructeur: :user)
.where(dossier_id: dossiers_ids)
.pluck('dossier_id, users.email')
.group_by { |dossier_id, _| dossier_id }
.to_h { |dossier_id, dossier_id_emails| [dossier_id, dossier_id_emails.map { |_, email| email }&.join(', ')] }
end
end
Dossier
.select(:id, :state, :archived) # the dossier object is needed in the view
.find(dossiers_ids) # keeps dossiers_ids order and raise exception if one is missing
.map do |dossier|
DossierProjection.new(dossier, fields.map { |f| f[:id_value_h][dossier.id] })
end
end
end

View file

@ -2,9 +2,9 @@
Bonjour, Bonjour,
%p %p
Vous avez demandé à regénérer votre mot de passe sur #{APPLICATION_BASE_URL}. Pour ceci, merci de suivre le lien suivant : Vous avez demandé à changer votre mot de passe sur #{APPLICATION_NAME}. Pour ceci, merci de cliquer sur le lien suivant :
%br
= link_to edit_password_url(@resource, reset_password_token: @token), edit_password_url(@resource, reset_password_token: @token) = round_button 'Changer mon mot de passe', edit_password_url(@resource, reset_password_token: @token), :primary
%p %p
Si vous n'avez pas effectué une telle demande, merci d'ignorer cet email. Votre mot de passe ne sera pas changé. Si vous n'avez pas effectué une telle demande, merci d'ignorer cet email. Votre mot de passe ne sera pas changé.

View file

@ -17,5 +17,5 @@
%br %br
%br %br
.actions .actions
= f.submit 'Réinitialiser', class: 'btn btn-primary' = f.submit 'Demander un nouveau mot de passe', class: 'button large expand primary'
%br %br

View file

@ -129,25 +129,27 @@
= submit_tag "Enregistrer", class: 'button' = submit_tag "Enregistrer", class: 'button'
%tbody %tbody
- @dossiers.each do |dossier| - @projected_dossiers.each do |p|
- dossier = p.dossier
- path = instructeur_dossier_path(@procedure, dossier.id)
%tr %tr
%td.folder-col %td.folder-col
= link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do %a.cell-link{ href: path }
%span.icon.folder %span.icon.folder
- if @not_archived_notifications_dossier_ids.include?(dossier.id) - if @not_archived_notifications_dossier_ids.include?(dossier.id)
%span.notifications{ 'aria-label': 'notifications' } %span.notifications{ 'aria-label': 'notifications' }
%td.number-col %td.number-col
= link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do %a.cell-link{ href: path }= dossier.id
= dossier.id
- @procedure_presentation.displayed_fields_values(dossier).each do |value| - p.columns.each do |column|
%td %td
= link_to(value, instructeur_dossier_path(@procedure, dossier), class: 'cell-link') %a.cell-link{ href: path }= column
%td.status-col %td.status-col
= link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do %a.cell-link{ href: path }= status_badge(dossier.state)
= status_badge(dossier.state)
%td.action-col.follow-col= render partial: 'dossier_actions', locals: { procedure: @procedure, dossier: dossier, dossier_is_followed: @followed_dossiers_id.include?(dossier.id) } %td.action-col.follow-col= render partial: 'dossier_actions', locals: { procedure: @procedure, dossier: dossier, dossier_is_followed: @followed_dossiers_id.include?(dossier.id) }
= paginate @dossiers = paginate @dossiers
- else - else

View file

@ -1,8 +1,12 @@
.sub-header .sub-header
.container.flex.justify-between.align-baseline .container.flex.justify-between.align-baseline.column
%ul.breadcrumbs %ul.breadcrumbs
- steps.each do |step| - steps.each do |step|
%li= step %li= step
- if defined?(preview) && preview - if defined?(preview) && preview
= link_to "Prévisualiser le formulaire", apercu_admin_procedure_path(@procedure), target: "_blank", rel: "noopener", class: 'button' = link_to "Prévisualiser le formulaire", apercu_admin_procedure_path(@procedure), target: "_blank", rel: "noopener", class: 'button'
= link_to "Continuer >", admin_procedure_path(@procedure), title: 'Vous pourrez revenir ici par la suite', class: 'button accepted' = link_to "Continuer >", admin_procedure_path(@procedure), title: 'Vous pourrez revenir ici par la suite', class: 'button accepted'
- if defined?(metadatas)
%ul.admin-metadata
- metadatas.each do |metadata|
%li= metadata

View file

@ -6,7 +6,7 @@
.flex.column.ml-1 .flex.column.ml-1
.card-title .card-title
= link_to procedure.libelle, admin_procedure_path(procedure), style: 'color: black;' = link_to procedure.libelle, admin_procedure_path(procedure), style: 'color: black;'
= link_to(procedure_lien(procedure), procedure_lien(procedure), class: 'procedure-lien mb-1') = link_to(procedure_lien(procedure), procedure_lien(procedure), class: 'mb-1')
.admin-procedures-list-timestamps .admin-procedures-list-timestamps
%p.notice N° #{procedure.id} %p.notice N° #{procedure.id}

View file

@ -1,6 +1,7 @@
= render partial: 'new_administrateur/breadcrumbs', = render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path), locals: { steps: [link_to('Démarches', admin_procedures_path),
"#{@procedure.libelle} (crée le #{@procedure.created_at.strftime('%d/%m/%Y')})", "#{@procedure.close? ? "Close" : @procedure.locked? ? "Publiée" : "Brouillon"}"] } "#{@procedure.libelle}", ],
metadatas: ["Créée le #{@procedure.created_at.strftime('%d/%m/%Y')} - n° #{@procedure.id}", "#{@procedure.close? ? "Close le #{@procedure.closed_at.strftime('%d/%m/%Y')}" : @procedure.locked? ? "Publiée - #{procedure_lien(@procedure)}" : "Brouillon"}"] }
.container.procedure-admin-container .container.procedure-admin-container
= link_to apercu_admin_procedure_path(@procedure), class: 'button', id: "preview-procedure" do = link_to apercu_admin_procedure_path(@procedure), class: 'button', id: "preview-procedure" do

View file

@ -14,4 +14,4 @@
= f.label :email, 'Email' = f.label :email, 'Email'
= f.email_field :email, autofocus: true = f.email_field :email, autofocus: true
= f.submit 'Réinitialiser', class: 'button primary' = f.submit 'Demander un nouveau mot de passe', class: 'button large expand primary'

View file

@ -9,9 +9,11 @@
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { class: 'form' }) do |f| = form_for(resource, as: resource_name, url: password_path(resource_name), html: { class: 'form' }) do |f|
%h1 Mot de passe oublié %h1= t('devise.passwords.new.forgot_your_password')
%p.notice= t('views.passwords.new.send_me_reset_password_instructions')
= f.label :email, 'Email' = f.label :email, 'Email'
= f.email_field :email, autofocus: true = f.email_field :email, autofocus: true
= f.submit 'Réinitialiser', class: 'button primary' = f.submit 'Demander un nouveau mot de passe', class: 'button expand primary'

View file

@ -0,0 +1,32 @@
- content_for(:title, t('views.users.passwords.reset_link_sent.title'))
- content_for :footer do
= render partial: 'root/footer'
#link-sent.container
= image_tag('user/confirmation-email.svg')
%h1
= t('views.users.passwords.reset_link_sent.got_it')
%br
= t('views.users.passwords.reset_link_sent.open_your_mailbox')
%section.link-sent-info
%p
= t('views.users.passwords.reset_link_sent.email_sent_html', email: @email)
%p
= t('views.users.passwords.reset_link_sent.click_link_to_reset_password')
%p
= t('views.users.shared.email_can_take_a_while_html')
%section.link-sent-help
%h2.link-sent-help-title= t('views.users.passwords.reset_link_sent.no_mail')
%ol.link-sent-help-list
%li
= t('views.users.passwords.reset_link_sent.check_spams')
%li
= t('views.users.passwords.reset_link_sent.check_account', email: @email, application_name: APPLICATION_NAME)
- if FranceConnectService.enabled?
%li
= t('views.users.passwords.reset_link_sent.check_france_connect_html', href: france_connect_particulier_path)
%p
= t('views.users.shared.contact_us_if_any_trouble_html', href: contact_url)

View file

@ -7,16 +7,14 @@
= image_tag('user/confirmation-email.svg') = image_tag('user/confirmation-email.svg')
%h1 Encore une petite étape :) %h1 Encore une petite étape :)
%p.mail %section.link-sent-info
Ouvrez votre boite email <b>#{@email}</b> puis cliquez sur le lien d'activation du message <b>Connexion sécurisée à #{APPLICATION_NAME}</b>. %p
%br Ouvrez votre boite email <strong>#{@email}</strong> puis cliquez sur le lien dactivation du message <strong>Connexion sécurisée à #{APPLICATION_NAME}</strong>.
%br %p
<b>Attention</b>, ce message peut mettre jusqu'à <b>15 minutes</b> pour arriver. = t('views.users.shared.email_can_take_a_while')
%p.help %section.link-sent-help
%p
Si vous voyez cette page trop souvent, consultez notre aide : #{link_to FAQ_CONFIRMER_COMPTE_CHAQUE_CONNEXION_URL, FAQ_CONFIRMER_COMPTE_CHAQUE_CONNEXION_URL, target: '_blank', rel: 'noopener' } Si vous voyez cette page trop souvent, consultez notre aide : #{link_to FAQ_CONFIRMER_COMPTE_CHAQUE_CONNEXION_URL, FAQ_CONFIRMER_COMPTE_CHAQUE_CONNEXION_URL, target: '_blank', rel: 'noopener' }
%br %p
%br = t('views.users.shared.contact_us_if_any_trouble_html', href: contact_admin_url)
En cas de difficultés, nous restons joignables
= succeed '.' do
= link_to("via ce formulaire", contact_admin_url)

View file

@ -2,7 +2,7 @@
.auth-form.sign-in-form .auth-form.sign-in-form
= form_for User.new, url: user_session_path, html: { class: "form" } do |f| = form_for resource, url: user_session_path, html: { class: "form" } do |f|
%h1.huge-title= t('views.sessions.new.title') %h1.huge-title= t('views.sessions.new.title')
= render partial: 'shared/france_connect_login', locals: { url: france_connect_particulier_path } = render partial: 'shared/france_connect_login', locals: { url: france_connect_particulier_path }
@ -14,7 +14,7 @@
= f.password_field :password, autocomplete: 'current-password' = f.password_field :password, autocomplete: 'current-password'
.auth-options .auth-options
%div .flex-no-shrink
= f.check_box :remember_me = f.check_box :remember_me
= f.label :remember_me, t('views.sessions.new.remember_me'), class: 'remember-me' = f.label :remember_me, t('views.sessions.new.remember_me'), class: 'remember-me'

View file

@ -1,5 +1,9 @@
Mailjet.configure do |config| ActiveSupport.on_load(:action_mailer) do
require 'mailjet'
Mailjet.configure do |config|
config.api_key = Rails.application.secrets.mailjet[:api_key] config.api_key = Rails.application.secrets.mailjet[:api_key]
config.secret_key = Rails.application.secrets.mailjet[:secret_key] config.secret_key = Rails.application.secrets.mailjet[:secret_key]
config.default_from = CONTACT_EMAIL config.default_from = CONTACT_EMAIL
end
end end

View file

@ -49,7 +49,7 @@ en:
new: new:
title: Sign in title: Sign in
email: Email address (name@site.com) email: Email address (name@site.com)
password: Password (minimum length %{min_length} characters) password: Password
remember_me: Remember me remember_me: Remember me
reset_password: Forgot password? reset_password: Forgot password?
connection: Sign in connection: Sign in
@ -60,6 +60,9 @@ en:
ligne1: A simple tool ligne1: A simple tool
ligne2: to manage dematerialized ligne2: to manage dematerialized
ligne3: administrative forms. ligne3: administrative forms.
passwords:
new:
send_me_reset_password_instructions: "Fill-in your account's email, and well send you instructions to reset your password."
modal: modal:
publish: publish:
title: title:

View file

@ -39,9 +39,9 @@ fr:
new: new:
title: Connectez-vous title: Connectez-vous
email: Email (nom@site.com) email: Email (nom@site.com)
password: Mot de passe (%{min_length} caractères minimum) password: Mot de passe
remember_me: Se souvenir de moi remember_me: Se souvenir de moi
reset_password: Mot de passe oublié ? reset_password: Mot de passe oublié ?
connection: Se connecter connection: Se connecter
are_you_new: Vous êtes nouveau sur %{app_name} ? are_you_new: Vous êtes nouveau sur %{app_name} ?
find_procedure: Trouvez votre démarche find_procedure: Trouvez votre démarche
@ -50,6 +50,24 @@ fr:
ligne1: Un outil simple ligne1: Un outil simple
ligne2: pour gérer les formulaires ligne2: pour gérer les formulaires
ligne3: administratifs dématérialisés. ligne3: administratifs dématérialisés.
passwords:
new:
send_me_reset_password_instructions: "Indiquez lemail de votre compte, et nous vous enverrons un lien pour créer un nouveau mot de passe."
users:
passwords:
reset_link_sent:
email_sent_html: "Nous vous avons envoyé un email à ladresse <strong>%{email}</strong>."
click_link_to_reset_password: "Cliquez sur le lien contenu dans lemail pour changer votre mot de passe."
no_mail: "Vous navez pas reçu lemail ?"
check_spams: "Vérifiez la boite Indésirables ou Spam de votre boite email."
check_account: "Avez-vous bien créé un compte %{application_name} avec ladresse %{email} ? Si aucun compte nexiste avec cette adresse, vous ne recevrez pas de message."
check_france_connect_html: "Vous êtes-vous connecté avec France Connect par le passé ? Dans ce cas <a href=\"%{href}\">essayez à nouveau avec France Connect</a>."
got_it: "Bien reçu !"
open_your_mailbox: "Maintenant ouvrez votre boite email."
title: "Lien de réinitialisation du mot de passe envoyé"
shared:
email_can_take_a_while_html: "<strong>Attention</strong>, ce message peut mettre jusquà <strong>15 minutes</strong> pour arriver."
contact_us_if_any_trouble_html: "En cas de difficultés, nous restons joignables <a href=\"%{href}\">via ce formulaire</a>."
modal: modal:
publish: publish:
title: title:

View file

@ -106,6 +106,7 @@ Rails.application.routes.draw do
get '/users/no_procedure' => 'users/sessions#no_procedure' get '/users/no_procedure' => 'users/sessions#no_procedure'
get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link' get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link'
get 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent' get 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent'
get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent'
end end
devise_scope :administrateur do devise_scope :administrateur do

View file

@ -0,0 +1,5 @@
class AddIndexToExerciceEtablissementId < ActiveRecord::Migration[6.1]
def change
add_index :exercices, :etablissement_id
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_04_16_074049) do ActiveRecord::Schema.define(version: 2021_04_16_160721) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -325,6 +325,7 @@ ActiveRecord::Schema.define(version: 2021_04_16_074049) do
t.datetime "date_fin_exercice" t.datetime "date_fin_exercice"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.index ["etablissement_id"], name: "index_exercices_on_etablissement_id"
end end
create_table "experts", force: :cascade do |t| create_table "experts", force: :cascade do |t|

View file

@ -38,4 +38,16 @@ describe Users::PasswordsController, type: :controller do
end end
end end
end end
describe '#reset_link_sent' do
let(:email) { 'test@example.com' }
it 'displays the page' do
get 'reset_link_sent', params: { email: email }
expect(response).to have_http_status(:ok)
expect(response).to render_template('reset_link_sent')
expect(assigns(:email)).to eq email
end
end
end end

View file

@ -42,7 +42,7 @@ feature 'Inviting an expert:', js: true do
expect(emails_sent_to(expert2.email.to_s).size).to eq(1) expect(emails_sent_to(expert2.email.to_s).size).to eq(1)
invitation_email = open_email(expert.email.to_s) invitation_email = open_email(expert.email.to_s)
avis = expert.avis.reload.last avis = expert.avis.find_by(dossier: dossier)
sign_up_link = sign_up_expert_avis_path(avis.dossier.procedure, avis, avis.expert.email) sign_up_link = sign_up_expert_avis_path(avis.dossier.procedure, avis, avis.expert.email)
expect(invitation_email.body).to include(sign_up_link) expect(invitation_email.body).to include(sign_up_link)
end end

View file

@ -6,8 +6,11 @@ feature 'Signin in:' do
visit root_path visit root_path
click_on 'Connexion' click_on 'Connexion'
sign_in_with user.email, password sign_in_with user.email, 'invalid-password'
expect(page).to have_content 'Courriel ou mot de passe incorrect.'
expect(page).to have_field('Email', with: user.email)
sign_in_with user.email, password
expect(page).to have_current_path dossiers_path expect(page).to have_current_path dossiers_path
end end

View file

@ -6,14 +6,15 @@ feature 'Managing password:' do
scenario 'a simple user can reset their password' do scenario 'a simple user can reset their password' do
visit root_path visit root_path
click_on 'Connexion' click_on 'Connexion'
click_on 'Mot de passe oublié ?' click_on 'Mot de passe oublié ?'
expect(page).to have_current_path(new_user_password_path) expect(page).to have_current_path(new_user_password_path)
fill_in 'Email', with: user.email fill_in 'Email', with: user.email
perform_enqueued_jobs do perform_enqueued_jobs do
click_on 'Réinitialiser' click_on 'Demander un nouveau mot de passe'
end end
expect(page).to have_content('Si votre courriel existe dans notre base de données, vous recevrez un lien vous permettant de récupérer votre mot de passe.') expect(page).to have_text 'Nous vous avons envoyé un email'
expect(page).to have_text user.email
click_reset_password_link_for user.email click_reset_password_link_for user.email
expect(page).to have_content 'Changement de mot de passe' expect(page).to have_content 'Changement de mot de passe'
@ -33,14 +34,15 @@ feature 'Managing password:' do
scenario 'an admin can reset their password' do scenario 'an admin can reset their password' do
visit root_path visit root_path
click_on 'Connexion' click_on 'Connexion'
click_on 'Mot de passe oublié ?' click_on 'Mot de passe oublié ?'
expect(page).to have_current_path(new_user_password_path) expect(page).to have_current_path(new_user_password_path)
fill_in 'Email', with: user.email fill_in 'Email', with: user.email
perform_enqueued_jobs do perform_enqueued_jobs do
click_on 'Réinitialiser' click_on 'Demander un nouveau mot de passe'
end end
expect(page).to have_content('Si votre courriel existe dans notre base de données, vous recevrez un lien vous permettant de récupérer votre mot de passe.') expect(page).to have_text 'Nous vous avons envoyé un email'
expect(page).to have_text user.email
click_reset_password_link_for user.email click_reset_password_link_for user.email

View file

@ -125,126 +125,6 @@ describe ProcedurePresentation do
it { expect(subject.displayed_fields_for_select).to eq([[["label1", "table1/column1"], ["label2", "table2/column2"]], ["user/email"]]) } it { expect(subject.displayed_fields_for_select).to eq([[["label1", "table1/column1"], ["label2", "table2/column2"]], ["user/email"]]) }
end end
describe '#get_value' do
let(:procedure_presentation) { create(:procedure_presentation, procedure: procedure, assign_to: assign_to, displayed_fields: [{ 'table' => table, 'column' => column }]) }
subject { procedure_presentation.displayed_fields_values(dossier).first }
context 'for self table' do
let(:table) { 'self' }
context 'for created_at column' do
let(:column) { 'created_at' }
let(:dossier) { Timecop.freeze(Time.zone.local(1992, 3, 22)) { create(:dossier, procedure: procedure) } }
it { is_expected.to eq('22/03/1992') }
end
context 'for en_construction_at column' do
let(:column) { 'en_construction_at' }
let(:dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: Time.zone.local(2018, 10, 17)) }
it { is_expected.to eq('17/10/2018') }
end
context 'for updated_at column' do
let(:column) { 'updated_at' }
let(:dossier) { create(:dossier, procedure: procedure) }
before { dossier.touch(time: Time.zone.local(2018, 9, 25)) }
it { is_expected.to eq('25/09/2018') }
end
end
context 'for user table' do
let(:table) { 'user' }
let(:column) { 'email' }
let(:dossier) { create(:dossier, procedure: procedure, user: create(:user, email: 'bla@yopmail.com')) }
it { is_expected.to eq('bla@yopmail.com') }
end
context 'for individual table' do
let(:table) { 'individual' }
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
let(:dossier) { create(:dossier, procedure: procedure, individual: create(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
context 'for prenom column' do
let(:column) { 'prenom' }
it { is_expected.to eq('Jacques') }
end
context 'for nom column' do
let(:column) { 'nom' }
it { is_expected.to eq('Martin') }
end
context 'for gender column' do
let(:column) { 'gender' }
it { is_expected.to eq('M.') }
end
end
context 'for etablissement table' do
let(:table) { 'etablissement' }
let(:column) { 'code_postal' } # All other columns work the same, no extra test required
let!(:dossier) { create(:dossier, procedure: procedure, etablissement: create(:etablissement, code_postal: '75008')) }
it { is_expected.to eq('75008') }
end
context 'for groupe_instructeur table' do
let(:table) { 'groupe_instructeur' }
let(:column) { 'label' }
let!(:dossier) { create(:dossier, procedure: procedure) }
it { is_expected.to eq('défaut') }
end
context 'for followers_instructeurs table' do
let(:table) { 'followers_instructeurs' }
let(:column) { 'email' }
let(:dossier) { create(:dossier, procedure: procedure) }
let!(:follow1) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'user1@host')) }
let!(:follow2) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'user2@host')) }
it "return emails of followers instructeurs" do
emails_to_display = procedure_presentation.displayed_fields_values(dossier).first.split(', ').sort
expect(emails_to_display).to eq ["user1@host", "user2@host"]
end
end
context 'for type_de_champ table' do
let(:table) { 'type_de_champ' }
let(:column) { procedure.types_de_champ.first.stable_id.to_s }
let(:dossier) { create(:dossier, procedure: procedure) }
before { dossier.champs.first.update(value: 'kale') }
it { is_expected.to eq('kale') }
end
context 'for type_de_champ_private table' do
let(:table) { 'type_de_champ_private' }
let(:column) { procedure.types_de_champ_private.first.stable_id.to_s }
let(:dossier) { create(:dossier, procedure: procedure) }
before { dossier.champs_private.first.update(value: 'quinoa') }
it { is_expected.to eq('quinoa') }
end
end
describe '#sorted_ids' do describe '#sorted_ids' do
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let(:assign_to) { create(:assign_to, procedure: procedure, instructeur: instructeur) } let(:assign_to) { create(:assign_to, procedure: procedure, instructeur: instructeur) }
@ -755,136 +635,6 @@ describe ProcedurePresentation do
end end
end end
describe '#eager_load_displayed_fields' do
let(:procedure_presentation) { create(:procedure_presentation, procedure: procedure, assign_to: assign_to, displayed_fields: [{ 'table' => table, 'column' => column }]) }
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
let(:displayed_dossier) { procedure_presentation.eager_load_displayed_fields(procedure.dossiers).first }
context 'for type de champ' do
let(:table) { 'type_de_champ' }
let(:column) { procedure.types_de_champ.first.stable_id.to_s }
it 'preloads the champs relation' do
# Ideally, we would only preload the champs for the matching column
expect(displayed_dossier.association(:champs)).to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).not_to be_loaded
end
end
context 'for type de champ private' do
let(:table) { 'type_de_champ_private' }
let(:column) { procedure.types_de_champ_private.first.stable_id.to_s }
it 'preloads the champs relation' do
# Ideally, we would only preload the champs for the matching column
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).not_to be_loaded
end
end
context 'for user' do
let(:table) { 'user' }
let(:column) { 'email' }
it 'preloads the user relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).not_to be_loaded
end
end
context 'for individual' do
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
let(:table) { 'individual' }
let(:column) { 'nom' }
it 'preloads the individual relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).not_to be_loaded
end
end
context 'for etablissement' do
let(:table) { 'etablissement' }
let(:column) { 'siret' }
it 'preloads the etablissement relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).not_to be_loaded
end
end
context 'for followers_instructeurs' do
let(:table) { 'followers_instructeurs' }
let(:column) { 'email' }
it 'preloads the followers_instructeurs relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).not_to be_loaded
end
end
context 'for groupe_instructeur' do
let(:table) { 'groupe_instructeur' }
let(:column) { 'label' }
it 'preloads the groupe_instructeur relation' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
expect(displayed_dossier.association(:groupe_instructeur)).to be_loaded
end
end
context 'for self' do
let(:table) { 'self' }
let(:column) { 'created_at' }
it 'does not preload anything' do
expect(displayed_dossier.association(:champs)).not_to be_loaded
expect(displayed_dossier.association(:champs_private)).not_to be_loaded
expect(displayed_dossier.association(:user)).not_to be_loaded
expect(displayed_dossier.association(:individual)).not_to be_loaded
expect(displayed_dossier.association(:etablissement)).not_to be_loaded
expect(displayed_dossier.association(:followers_instructeurs)).not_to be_loaded
end
end
end
describe "#human_value_for_filter" do describe "#human_value_for_filter" do
let(:filters) { { "suivis" => [{ "label" => "label1", "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "true" }] } } let(:filters) { { "suivis" => [{ "label" => "label1", "table" => "type_de_champ", "column" => first_type_de_champ_id, "value" => "true" }] } }

View file

@ -0,0 +1,157 @@
describe DossierProjectionService do
describe '#project' do
subject { described_class.project(dossiers_ids, fields) }
context 'with multiple dossier' do
let!(:procedure) { create(:procedure, :with_type_de_champ) }
let!(:dossier_1) { create(:dossier, procedure: procedure) }
let!(:dossier_2) { create(:dossier, procedure: procedure) }
let!(:dossier_3) { create(:dossier, procedure: procedure) }
let(:dossiers_ids) { [dossier_3.id, dossier_1.id, dossier_2.id] }
let(:fields) do
[
{
"table" => "type_de_champ",
"column" => procedure.types_de_champ[0].stable_id.to_s
}
]
end
before do
dossier_1.champs.first.update(value: 'champ_1')
dossier_2.champs.first.update(value: 'champ_2')
dossier_3.champs.first.destroy
end
let(:result) { subject }
it 'respects the dossiers_ids order and returns nil for empty result' do
expect(result.length).to eq(3)
expect(result[0].dossier.id).to eq(dossier_3.id)
expect(result[1].dossier.id).to eq(dossier_1.id)
expect(result[2].dossier.id).to eq(dossier_2.id)
expect(result[0].columns[0]).to be nil
expect(result[1].columns[0]).to eq('champ_1')
expect(result[2].columns[0]).to eq('champ_2')
end
end
context 'attributes by attributes' do
let(:fields) { [{ "table" => table, "column" => column }] }
let(:dossiers_ids) { [dossier.id] }
subject { super()[0].columns[0] }
context 'for self table' do
let(:table) { 'self' }
context 'for created_at column' do
let(:column) { 'created_at' }
let(:dossier) { Timecop.freeze(Time.zone.local(1992, 3, 22)) { create(:dossier) } }
it { is_expected.to eq('22/03/1992') }
end
context 'for en_construction_at column' do
let(:column) { 'en_construction_at' }
let(:dossier) { create(:dossier, :en_construction, en_construction_at: Time.zone.local(2018, 10, 17)) }
it { is_expected.to eq('17/10/2018') }
end
context 'for updated_at column' do
let(:column) { 'updated_at' }
let(:dossier) { create(:dossier) }
before { dossier.touch(time: Time.zone.local(2018, 9, 25)) }
it { is_expected.to eq('25/09/2018') }
end
end
context 'for user table' do
let(:table) { 'user' }
let(:column) { 'email' }
let(:dossier) { create(:dossier, user: create(:user, email: 'bla@yopmail.com')) }
it { is_expected.to eq('bla@yopmail.com') }
end
context 'for individual table' do
let(:table) { 'individual' }
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
let(:dossier) { create(:dossier, procedure: procedure, individual: create(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
context 'for prenom column' do
let(:column) { 'prenom' }
it { is_expected.to eq('Jacques') }
end
context 'for nom column' do
let(:column) { 'nom' }
it { is_expected.to eq('Martin') }
end
context 'for gender column' do
let(:column) { 'gender' }
it { is_expected.to eq('M.') }
end
end
context 'for etablissement table' do
let(:table) { 'etablissement' }
let(:column) { 'code_postal' } # All other columns work the same, no extra test required
let!(:dossier) { create(:dossier, etablissement: create(:etablissement, code_postal: '75008')) }
it { is_expected.to eq('75008') }
end
context 'for groupe_instructeur table' do
let(:table) { 'groupe_instructeur' }
let(:column) { 'label' }
let!(:dossier) { create(:dossier) }
it { is_expected.to eq('défaut') }
end
context 'for followers_instructeurs table' do
let(:table) { 'followers_instructeurs' }
let(:column) { 'email' }
let(:dossier) { create(:dossier) }
let!(:follow1) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'user1@host')) }
let!(:follow2) { create(:follow, dossier: dossier, instructeur: create(:instructeur, email: 'user2@host')) }
it { is_expected.to eq "user1@host, user2@host" }
end
context 'for type_de_champ table' do
let(:table) { 'type_de_champ' }
let(:dossier) { create(:dossier) }
let(:column) { dossier.procedure.types_de_champ.first.stable_id.to_s }
before { dossier.champs.first.update(value: 'kale') }
it { is_expected.to eq('kale') }
end
context 'for type_de_champ_private table' do
let(:table) { 'type_de_champ_private' }
let(:dossier) { create(:dossier) }
let(:column) { dossier.procedure.types_de_champ_private.first.stable_id.to_s }
before { dossier.champs_private.first.update(value: 'quinoa') }
it { is_expected.to eq('quinoa') }
end
end
end
end

View file

@ -3,7 +3,7 @@ describe 'users/sessions/new.html.haml', type: :view do
before(:each) do before(:each) do
allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user]) allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
allow(view).to receive(:resource_name).and_return(:user) allow(view).to receive(:resource).and_return(:user)
end end
before do before do

View file

@ -11617,9 +11617,9 @@ sshpk@^1.7.0:
tweetnacl "~0.14.0" tweetnacl "~0.14.0"
ssri@^6.0.1: ssri@^6.0.1:
version "6.0.1" version "6.0.2"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
dependencies: dependencies:
figgy-pudding "^3.5.1" figgy-pudding "^3.5.1"