accessibilite(pages-authentification): evolutions des pages de connexion/creation de compte pour respecter le DSFR et supporter une meilleure accessibilite

Update app/components/dsfr/input_component/input_component.html.haml

Co-authored-by: Colin Darie <colin@darie.eu>
This commit is contained in:
Martin 2022-12-20 17:51:36 +01:00 committed by mfo
parent be5b8c2683
commit a4d6692bc6
49 changed files with 314 additions and 439 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -21,10 +21,6 @@
padding-top: $default-spacer;
}
h1 {
margin-bottom: $default-spacer;
}
.form label {
margin-bottom: $default-spacer / 2;
}

View file

@ -1,53 +0,0 @@
@import "colors";
@import "constants";
.france-connect-login {
h2 {
color: $black;
}
}
a.france-connect-login-button {
display: inline-block;
height: 60px;
width: 230px;
margin: auto;
margin-bottom: 8px;
background-image: image-url("franceconnect-btn.svg"), image-url("franceconnect-btn-hover.svg");
background-repeat: no-repeat;
background-size: cover;
cursor: pointer;
font-size: 0;
&:hover {
background-image: image-url("franceconnect-btn-hover.svg");
}
}
.france-connect-login-separator {
display: flex;
flex-basis: 100%;
align-items: center;
color: $black;
text-transform: uppercase;
padding-bottom: $default-spacer;
padding-top: $default-spacer;
&::before,
&::after {
content: "";
flex-grow: 1;
background: $dark-grey;
height: 1px;
font-size: 0;
line-height: 0;
}
&::before {
margin-right: $default-spacer;
}
&::after {
margin-left: $default-spacer;
}
}

View file

@ -1,24 +0,0 @@
@import "colors";
@import "constants";
.suspect-email {
background-color: $orange-bg;
text-align: center;
margin-top: -$default-padding * 2;
margin-bottom: $default-padding * 2;
padding: ($default-padding - 2) $default-padding $default-padding $default-padding;
border-radius: 0 0 5px 5px;
}
.email-suggestion-address {
font-weight: bold;
}
.email-suggestion-title {
margin-bottom: $default-spacer;
}
.email-suggestion-answer button {
margin: 0 $default-spacer / 2;
min-width: 66px;
}

View file

@ -1,4 +1,11 @@
class Dsfr::InputComponent < ApplicationComponent
delegate :object, to: :@form
delegate :errors, to: :object
# use it to indicate detailed about the inputs, ex: https://www.systeme-de-design.gouv.fr/elements-d-interface/modeles-et-blocs-fonctionnels/demande-de-mot-de-passe
# it uses aria-describedby on input and link it to yielded content
renders_one :describedby
def initialize(form:, attribute:, input_type:, opts: {}, required: true)
@form = form
@attribute = attribute
@ -7,60 +14,93 @@ class Dsfr::InputComponent < ApplicationComponent
@required = required
end
# add invalid class on input when input is invalid
# and and valid on input only if another input is invalid
def input_group_opts
opts = {
class: class_names('fr-input-group': true,
'fr-password': password?,
"fr-input-group--error": errors_on_attribute?,
"fr-input-group--valid": !errors_on_attribute? && errors_on_another_attribute?)
}
if email?
opts[:data] = { controller: 'email-input' }
end
opts
end
def label_opts
{ class: class_names('fr-label': true, 'fr-password__label': password?) }
end
def input_opts
@opts[:class] = class_names(map_array_to_hash_with_true(@opts[:class])
.merge('fr-input': true,
.merge('fr-password__input': password?,
'fr-input': true,
'fr-mb-0': true,
'fr-input--error': errors_on_attribute?))
if errors_on_attribute?
if errors_on_attribute? || describedby
@opts = @opts.deep_merge(aria: {
describedby: error_message_id,
invalid: true
invalid: errors_on_attribute?
})
end
if @required
@opts[:required] = true
end
if email?
@opts = @opts.deep_merge(data: {
action: "blur->email-input#checkEmail",
'email-input-target': 'input'
})
end
@opts
end
# add invalid class on input when input is invalid
# and and valid on input only if another input is invalid
def input_group_class_names
class_names('fr-input-group': true,
"fr-input-group--error": errors_on_attribute?,
"fr-input-group--valid": !errors_on_attribute? && errors_on_another_attribute?)
end
# tried to inline it within the template, but failed miserably with a double render
def label
label = @form.object.class.human_attribute_name(@attribute)
if @required
label += tag.span(" *", class: 'mandatory')
end
label
end
# errors helpers
def errors_on_attribute?
@form.object.errors.has_key?(attribute_or_rich_body)
errors.has_key?(attribute_or_rich_body)
end
def error_message_id
dom_id(@form.object, @attribute)
dom_id(object, @attribute)
end
def error_messages
@form.object.errors.full_messages_for(attribute_or_rich_body)
errors.full_messages_for(attribute_or_rich_body)
end
# i18n lookups
def label
object.class.human_attribute_name(@attribute)
end
def hint
I18n.t("activerecord.attributes.#{object.class.name.underscore}.hints.#{@attribute}")
end
# kind of input helpers
def password?
@input_type == :password_field
end
def email?
@input_type == :email_field
end
private
def errors_on_another_attribute?
!@form.object.errors.empty?
def hint?
I18n.exists?("activerecord.attributes.#{object.class.name.underscore}.hints.#{@attribute}")
end
def errors_on_another_attribute?
!errors.empty?
end
# lookup for edge case from `form.rich_text_area`
# rich text uses _rich_#{attribute}, but it is saved on #{attribute}, as well as error messages
def attribute_or_rich_body
case @input_type
when :rich_text_area

View file

@ -0,0 +1,7 @@
---
en:
show_password:
aria_label: "Show password"
label: "Show"
email_suggest:
wanna_say: 'Do you mean to say ?'

View file

@ -0,0 +1,7 @@
---
fr:
show_password:
aria_label: "Afficher le mot de passe"
label: "Afficher"
email_suggest:
wanna_say: 'Voulez-vous dire ?'

View file

@ -1,5 +1,11 @@
%div{ class: input_group_class_names }
= @form.label @attribute, label.html_safe, class: "fr-label"
= content_tag(:div, input_group_opts) do
= @form.label @attribute, label_opts do
- capture do
= label
- if @required
%span.mandatory  *
- if hint?
%span.fr-hint-text= hint
= @form.send(@input_type, @attribute, input_opts)
@ -12,3 +18,21 @@
- error_messages.map do |error_message|
%li= error_message
- elsif describedby.present?
= describedby
- if password?
.fr-password__checkbox.fr-checkbox-group.fr-checkbox-group--sm
%input#show_password{ "aria-label" => t('.show_password.aria_label'), type: "checkbox" }/
%label.fr--password__checkbox.fr-label{ for: "show_password" }= t('.show_password.label')
- if email?
.suspect-email.hidden{ data: { "email-input-target": 'ariaRegion'}, aria: { live: 'off' } }
= render Dsfr::AlertComponent.new(title: t('.email_suggest.wanna_say'), state: :info, heading_level: :div) do |c|
- c.body do
%p{ data: { "email-input-target": 'suggestion'} } exemple@gmail.com &nbsp;?
%p
= button_tag type: 'button', class: 'fr-btn fr-btn--sm fr-mr-3w', data: { action: 'click->email-input#accept'} do
= t('utils.yes')
= button_tag type: 'button', class: 'fr-btn fr-btn--sm', data: { action: 'click->email-input#discard'} do
= t('utils.no')

View file

@ -0,0 +1,33 @@
import { suggest } from 'email-butler';
import { show, hide } from '@utils';
import { ApplicationController } from './application_controller';
export class EmailInputController extends ApplicationController {
static targets = ['ariaRegion', 'suggestion', 'input'];
declare readonly ariaRegionTarget: HTMLElement;
declare readonly suggestionTarget: HTMLElement;
declare readonly inputTarget: HTMLInputElement;
checkEmail() {
const suggestion = suggest(this.inputTarget.value);
if (suggestion && suggestion.full) {
this.suggestionTarget.innerHTML = suggestion.full;
show(this.ariaRegionTarget);
this.ariaRegionTarget.setAttribute('aria-live', 'assertive');
}
}
accept() {
this.ariaRegionTarget.setAttribute('aria-live', 'off');
hide(this.ariaRegionTarget);
this.inputTarget.value = this.suggestionTarget.innerHTML;
this.suggestionTarget.innerHTML = '';
}
discard() {
this.ariaRegionTarget.setAttribute('aria-live', 'off');
hide(this.ariaRegionTarget);
this.suggestionTarget.innerHTML = '';
}
}

View file

@ -24,10 +24,6 @@ import {
motivationCancel,
showImportJustificatif
} from '../new_design/state-button';
import {
acceptEmailSuggestion,
discardEmailSuggestionBox
} from '../new_design/user-sign_up';
import { showFusion, showNewAccount } from '../new_design/fc-fusion';
const application = Application.start();
@ -41,9 +37,7 @@ const DS = {
showImportJustificatif,
showFusion,
showNewAccount,
replaceSemicolonByComma,
acceptEmailSuggestion,
discardEmailSuggestionBox
replaceSemicolonByComma
};
// Start Rails helpers

View file

@ -31,3 +31,4 @@
@import '@gouvfr/dsfr/dist/component/translate/translate.css';
@import '@gouvfr/dsfr/dist/component/pagination/pagination.css';
@import '@gouvfr/dsfr/dist/component/skiplink/skiplink.css';
@import '@gouvfr/dsfr/dist/component/password/password.css';

View file

@ -1,35 +0,0 @@
import { delegate, show, hide } from '@utils';
import { suggest } from 'email-butler';
const userNewEmailSelector = '#new_user input#user_email';
const passwordFieldSelector = '#new_user input#user_password';
const suggestionsSelector = '.suspect-email';
const emailSuggestionSelector = '.suspect-email .email-suggestion-address';
delegate('focusout', userNewEmailSelector, () => {
// When the user leaves the email input during account creation, we check if this account looks correct.
// If not (e.g if its "bidou@gmail.coo" or "john@yahoo.rf"), we attempt to suggest a fix for the invalid email.
const userEmailInput = document.querySelector(userNewEmailSelector);
const suggestedEmailSpan = document.querySelector(emailSuggestionSelector);
const suggestion = suggest(userEmailInput.value);
if (suggestion && suggestion.full && suggestedEmailSpan) {
suggestedEmailSpan.innerHTML = suggestion.full;
show(document.querySelector(suggestionsSelector));
} else {
hide(document.querySelector(suggestionsSelector));
}
});
export function acceptEmailSuggestion() {
const userEmailInput = document.querySelector(userNewEmailSelector);
const suggestedEmailSpan = document.querySelector(emailSuggestionSelector);
userEmailInput.value = suggestedEmailSpan.innerHTML;
hide(document.querySelector(suggestionsSelector));
document.querySelector(passwordFieldSelector).focus();
}
export function discardEmailSuggestionBox() {
hide(document.querySelector(suggestionsSelector));
}

View file

@ -26,8 +26,7 @@
= link_to t('.whats_agentconnect'), 'https://agentconnect.gouv.fr/', target: '_blank', rel: "noopener"
.france-connect-login-separator
= t('views.shared.france_connect_login.separator')
%p.fr-hr-or= t('views.shared.france_connect_login.separator')
#session-new.auth-form.sign-in-form
= form_for User.new, url: user_session_path, html: { class: "form" } do |f|

View file

@ -1,6 +1,6 @@
- if FranceConnectService.enabled?
.france-connect-login
%h2.fr-h6.mb-0
%h2.fr-h6
= t('views.shared.france_connect_login.title')
%p
= t('views.shared.france_connect_login.description')
@ -13,7 +13,6 @@
%p
= link_to t('views.shared.france_connect_login.help_link'), "https://franceconnect.gouv.fr/", title: new_tab_suffix(t('views.shared.france_connect_login.help_link')), **external_link_attributes
.france-connect-login-separator
= t('views.shared.france_connect_login.separator')
%p.fr-hr-or= t('views.shared.france_connect_login.separator')
- else
<!-- FranceConnect is not configured -->

View file

@ -2,26 +2,26 @@
.auth-form
= devise_error_messages!
= form_for resource, url: user_registration_path, html: { class: "form" } do |f|
= form_for resource, url: user_registration_path, html: { class: "fr-py-5w" } do |f|
%h1.fr-h2= t('views.registrations.new.title', name: APPLICATION_NAME)
= render partial: 'shared/france_connect_login', locals: { url: france_connect_particulier_path }
= f.label :email, t('views.registrations.new.email_label'), id: :user_email_label
= f.text_field :email, type: :email, autocomplete: 'email', autofocus: true, placeholder: t('views.registrations.new.email_placeholder'), 'aria-describedby': :user_email_label
%fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'create-account-legend' } }
%legend.fr-fieldset__legend#create-account-legend
%h2.fr-h6= I18n.t('views.registrations.new.subtitle')
.suspect-email.hidden
.email-suggestion-title
= t('views.registrations.new.wanna_say')
%span.email-suggestion-address blabla@gmail.com
&nbsp;?
.email-suggestion-answer
= button_tag type: 'button', class: 'button small', onclick: "DS.acceptEmailSuggestion()" do
= t('utils.yes')
= button_tag type: 'button', class: 'button small', onclick: "DS.discardEmailSuggestionBox()" do
= t('utils.no')
.fr-fieldset__element
%p.fr-text--sm= t('utils.asterisk_html')
= f.label :password, t('views.registrations.new.password_label', min_length: PASSWORD_MIN_LENGTH), id: :user_password_label
= f.password_field :password, autocomplete: 'new-password', value: @user.password, placeholder: t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH), 'aria-describedby': :user_password_label
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
= f.submit t('views.shared.account.create'), class: "button large primary expand"
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', min_length: PASSWORD_MIN_LENGTH }) do |c|
- c.describedby do
#password-input-messages.fr-messages-group{ "aria-live" => "off" }
%p#password-input-message.fr-message= t('views.registrations.new.password_message')
%p#password-input-message-info.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH)
= f.submit t('views.shared.account.create'), class: "fr-btn fr-btn--lg"

View file

@ -1,32 +1,40 @@
= content_for(:page_id, 'auth')
= content_for(:title, t('metas.signin.title'))
#session-new.auth-form.sign-in-form
= form_for resource, url: user_session_path, html: { class: "form" } do |f|
%h1.huge-title= t('views.users.sessions.new.sign_in')
#session-new.auth-form.sign-in-form
= form_for resource, url: user_session_path, html: { class: "fr-py-5w" } do |f|
%h1.fr-h2= t('views.users.sessions.new.sign_in', application_name: APPLICATION_NAME)
= render partial: 'shared/france_connect_login', locals: { url: france_connect_particulier_path }
= f.label :email, t('views.users.sessions.new.email')
= f.text_field :email, type: :email, autocomplete: 'email', autofocus: true
%fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'new-account-legend' } }
%legend.fr-fieldset__legend#new-account-legend
%h2.fr-h6= I18n.t('views.users.sessions.new.subtitle')
= f.label :password, t('views.users.sessions.new.password', min_length: PASSWORD_MIN_LENGTH)
= f.password_field :password, autocomplete: 'current-password'
.fr-fieldset__element
%p.fr-text--sm= t('utils.asterisk_html')
.fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true })
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'current-password' })
%p= link_to t('views.users.sessions.new.reset_password'), new_user_password_path, class: "link"
.fr-fieldset__element
.auth-options
.flex-no-shrink
= f.check_box :remember_me
= f.label :remember_me, t('views.users.sessions.new.remember_me'), class: 'remember-me'
.text-right
= link_to t('views.users.sessions.new.reset_password'), new_user_password_path, class: "link"
= f.submit t('views.users.sessions.new.connection'), class: "fr-btn fr-btn--lg"
- if AgentConnectService.enabled?
.france-connect-login-separator
= t('views.shared.france_connect_login.separator')
%p.fr-hr-or= t('views.shared.france_connect_login.separator')
.center
%h2.important-header.mb-1= t('views.users.sessions.new.state_civil_servant')
= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path, class: "fr-btn fr-btn--secondary"

View file

@ -92,5 +92,11 @@ module TPS
config.view_component.show_previews_source = true
config.view_component.default_preview_layout = 'component_preview'
config.view_component.preview_paths << "#{Rails.root}/spec/components/previews"
# rubocop:disable Rails/OutputSafety
config.action_view.field_error_proc = Proc.new do |html_tag, _instance|
html_tag.html_safe # this is generated by rails
end
# rubocop:enable Rails/OutputSafety
end
end

View file

@ -97,6 +97,7 @@ search:
## Consider these keys used:
ignore_unused:
- 'errors.format'
- 'activerecord.models.*'
- 'activerecord.attributes.*'
- 'activerecord.errors.*'

View file

@ -32,7 +32,6 @@
en:
invisible_captcha:
sentence_for_humans: 'If you are a human, ignore this field'
help: 'Help'
help_dropdown:
general_title: "Online help"
@ -141,10 +140,10 @@ en:
registrations:
new:
title: "Create an account %{name}"
subtitle: "Create an account using an email"
email_label: 'Email (name@site.com)'
email_placeholder: 'Your email address'
wanna_say: 'Do you mean to say'
password_label: "Password (%{min_length} characters minimum)"
password_message: "Your password must have :"
password_placeholder: "%{min_length} characters minimum"
invites:
dropdown:
@ -314,8 +313,7 @@ en:
actions: "Actions"
sessions:
new:
sign_in: Sign in
email: Email address (name@site.com)
sign_in: Sign in on %{application_name}
password: Password (minimum length %{min_length} characters)
remember_me: Remember me
reset_password: Forgot password?
@ -323,6 +321,7 @@ en:
find_procedure: Find your procedure
state_civil_servant: Are you a state civil servant?
connect_with_agent_connect: Visit our dedicated page
subtitle: "Sign in with my account"
passwords:
reset_link_sent:
got_it: Got it!
@ -352,10 +351,14 @@ en:
user:
one: User
other: Users
attributes:
default_attributes: &default_attributes
password: 'password'
email: Email
password: 'Password'
requested_merge_into: 'new email address'
hints:
email: "Expected format : john.doe@example.com"
user:
siret: 'SIRET number'
<< : *default_attributes
@ -363,8 +366,11 @@ en:
<< : *default_attributes
super_admin:
<< : *default_attributes
instructeur:
password: 'password'
procedure:
zone: This procedure is run by
champs:
value: Value
errors:
messages:
not_a_phone: 'Invalid phone number'
@ -374,35 +380,40 @@ en:
attributes:
footer:
too_long: ": the footer is too long."
user:
attributes:
reset_password_token:
# invalid: ": Votre lien de nouveau mot de passe a expiré. Merci den demander un nouveau."
email:
invalid: invalid
taken: already in use
password:
too_short: 'is too short'
password_confirmation:
confirmation: ': The two passwords do not match'
requested_merge_into:
same: "can't be the same as the old one"
invite:
attributes:
email:
taken: ': Invitation already sent'
user:
attributes: &error_attributes
reset_password_token:
invalid: ": is expired. Ask a new one"
email:
blank: "is empty. Fill in the email"
invalid: "is invalid. Fill in a valid email address, example: john.doe@example.fr"
taken: "already in use. Fill in another email"
password:
too_short: "is too short. Fill in a password with at least 8 characters"
not_strong: "not strong enough. Fill in a stronger password"
password_confirmation:
confirmation: ": The two passwords do not match"
requested_merge_into:
same: "can't be the same as the old one"
instructeur:
attributes:
email:
invalid: invalid
taken: already in use
password:
too_short: 'is too short'
<< : *error_attributes
super_admin:
attributes:
<< : *error_attributes
procedure:
attributes:
path:
taken: is already used for procedure. You cannot use it because it belongs to another administrator.
# taken_can_be_claimed: est identique à celui dune autre de vos procedures publiées. Si vous publiez cette procedure, lancienne sera dépubliée et ne sera plus accessible au public. Les utilisateurs qui ont commencé un brouillon vont pouvoir le déposer.
taken_can_be_claimed: Is the same as another of your procedure. If you publish this procedure, the other one will be unpublished
invalid: is not valid. It must countain between 3 and 50 characters among a-z, 0-9, '_' and '-'.
"champs/cnaf_champ":
attributes:
@ -417,6 +428,7 @@ en:
reference_avis:
invalid: "must be 13 or 14 characters long"
errors:
format: "Field « %{attribute} » %{message}"
messages:
dossier_not_found: "The file does not exist or you do not have access to it."
# # dossier_map_not_activated: "The file does not have access to the map."

View file

@ -132,11 +132,11 @@ fr:
prefill_link_copy: Copier le lien de préremplissage
registrations:
new:
title: "Créez-vous un compte %{name}"
title: "Creation de compte sur %{name}"
subtitle: "Se créer un compte en choisissant un identifiant"
email_label: 'Email'
email_placeholder: 'Votre adresse email'
wanna_say: 'Voulez-vous dire'
password_label: "Mot de passe (%{min_length} caractères minimum)"
password_message: "Votre mot de passe doit contenir :"
password_placeholder: "%{min_length} caractères minimum"
invites:
dropdown:
@ -310,8 +310,7 @@ fr:
actions: "Actions"
sessions:
new:
sign_in: Connectez-vous
email: Email
sign_in: Connexion à %{application_name}
password: Mot de passe (%{min_length} caractères minimum)
remember_me: Se souvenir de moi
reset_password: Mot de passe oublié ?
@ -319,6 +318,7 @@ fr:
find_procedure: Trouvez votre démarche
state_civil_servant: Vous êtes agent de la fonction publique dÉtat ?
connect_with_agent_connect: Accédez à notre page dédiée
subtitle: "Se connecter avec son compte"
passwords:
reset_link_sent:
email_sent_html: "Nous vous avons envoyé un email à ladresse <strong>%{email}</strong>."
@ -349,10 +349,14 @@ fr:
user:
one: Utilisateur
other: Utilisateurs
attributes:
default_attributes: &default_attributes
password: 'Le mot de passe'
email: Adresse éléctronique
password: 'Mot de passe'
requested_merge_into: 'La nouvelle adresse email'
hints:
email: "Format attendu : john.doe@exemple.fr"
user:
siret: 'Numéro SIRET'
<< : *default_attributes
@ -364,6 +368,7 @@ fr:
zone: La démarche est mise en œuvre par
champs:
value: Valeur du champ
errors:
messages:
not_a_phone: 'Numéro de téléphone invalide'
@ -373,35 +378,33 @@ fr:
attributes:
footer:
too_long: ": le pied de page est trop long."
user:
attributes:
reset_password_token:
invalid: ": Votre lien de nouveau mot de passe a expiré. Merci den demander un nouveau."
email:
invalid: invalide
taken: déjà utilisé
password: &password
too_short: 'est trop court'
not_strong: 'nest pas assez complexe'
password_confirmation:
confirmation: ': Les deux mots de passe ne correspondent pas'
requested_merge_into:
same: "ne peut être identique à lancienne"
invite:
attributes:
email:
taken: ': Invitation déjà envoyée'
user:
attributes: &error_attributes
reset_password_token:
invalid: ": Votre lien de nouveau mot de passe a expiré. Merci den demander un nouveau."
email:
blank: "est vide. Saisir une adresse éléctronique"
invalid: "est invalide. Saisir une adresse éléctronique valide, exemple : john.doe@exemple.fr"
taken: "déjà utilisé. Saisir une autre adresse éléctronique."
password:
too_short: "est trop court. Saisir un mot de passe avec au moins 8 caractères"
not_strong: "nest pas assez complexe. Saisir un mot de passe plus complexe"
password_confirmation:
confirmation: ': Les deux mots de passe ne correspondent pas'
requested_merge_into:
same: "ne peut être identique à lancienne. Saisir une autre adresse email"
instructeur:
attributes:
email:
invalid: invalide
taken: déjà utilisé
password:
too_short: 'est trop court'
<< : *error_attributes
super_admin:
attributes:
password:
<< : *password
<< : *error_attributes
procedure:
attributes:
path:
@ -420,7 +423,9 @@ fr:
invalid: "doit posséder 13 ou 14 caractères"
reference_avis:
invalid: "doit posséder 13 ou 14 caractères"
errors:
format: "Le champ « %{attribute} » %{message}"
messages:
saml_not_authorized: "Vous nêtes pas autorisé à accéder à ce service."
dossier_not_found: "Le dossier nexiste pas ou vous ny avez pas accès."

View file

@ -10,6 +10,6 @@ fr:
siret:
attributes:
siret:
length: 'Le numéro SIRET doit comporter 14 chiffres'
checksum: 'Le numéro SIRET comporte une erreur, vérifiez les chiffres composant le numéro'
invalid: 'Le numéro SIRET ne correspond pas à un établissement existant'
length: 'est invalide. Saisir un numéro SIRET avec 14 chiffres'
checksum: 'comporte une erreur. Vérifier les chiffres composant le numéro'
invalid: 'ne correspond pas à un établissement existant'

View file

@ -10,7 +10,7 @@ fr:
reponse_donnee_le: "Réponse donnée le %{date}"
en_attente: "En attente de réponse"
france_connect_login:
title: 'Avec FranceConnect'
title: 'Se créer un compte avec FranceConnect'
description: "FranceConnect est la solution proposée par lÉtat pour sécuriser et simplifier la connexion aux services en ligne."
login_button: "Sidentifier avec"
help_link: "Quest-ce que FranceConnect ?"

View file

@ -58,7 +58,7 @@ describe Administrateurs::TypesDeChampController, type: :controller do
it do
is_expected.to have_http_status(:ok)
expect(assigns(:coordinate)).to be_nil
expect(flash.alert).to eq(["Libelle doit être rempli"])
expect(flash.alert).to eq(["Le champ « Libelle » doit être rempli"])
end
end
end
@ -94,7 +94,7 @@ describe Administrateurs::TypesDeChampController, type: :controller do
it do
is_expected.to have_http_status(:ok)
expect(assigns(:coordinate)).to be_nil
expect(flash.alert).to eq(["Libelle doit être rempli"])
expect(flash.alert).to eq(["Le champ « Libelle » doit être rempli"])
end
end
end

View file

@ -949,7 +949,7 @@ describe API::V2::GraphqlController do
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierEnvoyerMessage: {
errors: [{ message: "Votre message ne peut être vide" }],
errors: [{ message: "Le champ « Votre message » ne peut être vide" }],
message: nil
})
end

View file

@ -360,7 +360,7 @@ describe Experts::AvisController, type: :controller do
it do
expect(response).to render_template :instruction
expect(flash.alert).to eq(["toto.fr : Email n'est pas valide"])
expect(flash.alert).to eq(["toto.fr : Le champ « Email » n'est pas valide"])
expect(Avis.last).to eq(previous_avis)
expect(dossier.last_avis_updated_at).to eq(nil)
end
@ -382,7 +382,7 @@ describe Experts::AvisController, type: :controller do
it do
expect(response).to render_template :instruction
expect(flash.alert).to eq(["toto.fr : Email n'est pas valide"])
expect(flash.alert).to eq(["toto.fr : Le champ « Email » n'est pas valide"])
expect(flash.notice).to eq("Une demande davis a été envoyée à titi@titimail.com")
expect(Avis.count).to eq(old_avis_count + 1)
end

View file

@ -608,7 +608,7 @@ describe Instructeurs::DossiersController, type: :controller do
before { subject }
it { expect(response).to render_template :avis }
it { expect(flash.alert).to eq(["emaila.com : Email n'est pas valide"]) }
it { expect(flash.alert).to eq(["emaila.com : Le champ « Email » n'est pas valide"]) }
it { expect { subject }.not_to change(Avis, :count) }
it { expect(dossier.last_avis_updated_at).to eq(nil) }
end
@ -619,7 +619,7 @@ describe Instructeurs::DossiersController, type: :controller do
before { subject }
it { expect(response).to render_template :avis }
it { expect(flash.alert).to eq(["toto.fr : Email n'est pas valide"]) }
it { expect(flash.alert).to eq(["toto.fr : Le champ « Email » n'est pas valide"]) }
it { expect(flash.notice).to eq("Une demande davis a été envoyée à titi@titimail.com") }
it { expect(Avis.count).to eq(old_avis_count + 1) }
it { expect(saved_avis.expert.email).to eq("titi@titimail.com") }

View file

@ -40,7 +40,7 @@ describe Manager::UsersController, type: :controller do
subject
expect(User.find_by(id: user.id).email).not_to eq(nouvel_email)
expect(flash[:error]).to match("Courriel invalide")
expect(flash[:error]).to match("Le champ « Adresse éléctronique » est invalide. Saisir une adresse éléctronique valide, exemple : john.doe@exemple.fr")
end
end
end

View file

@ -169,7 +169,7 @@ describe Users::DossiersController, type: :controller do
it do
expect(response).not_to have_http_status(:redirect)
expect(flash[:alert]).to include("Civilité doit être rempli", "Nom doit être rempli", "Prénom doit être rempli")
expect(flash[:alert]).to include("Le champ « Civilité » doit être rempli", "Le champ « Nom » doit être rempli", "Le champ « Prénom » doit être rempli")
end
end
end
@ -239,7 +239,7 @@ describe Users::DossiersController, type: :controller do
context 'with an invalid SIRET' do
let(:params_siret) { '000 000' }
it_behaves_like 'the request fails with an error', ['Siret Le numéro SIRET doit comporter 14 chiffres']
it_behaves_like 'the request fails with an error', ['Le champ « Siret » est invalide. Saisir un numéro SIRET avec 14 chiffres']
end
context 'with a valid SIRET' do

View file

@ -36,7 +36,7 @@ describe Users::ProfilController, type: :controller do
it 'fails' do
patch :update_email, params: { user: { email: user.email } }
expect(response).to have_http_status(302)
expect(flash[:alert]).to eq(["La nouvelle adresse email ne peut être identique à lancienne"])
expect(flash[:alert]).to eq(["Le champ « La nouvelle adresse email » ne peut être identique à lancienne. Saisir une autre adresse email"])
end
end
@ -83,7 +83,7 @@ describe Users::ProfilController, type: :controller do
end
it { expect(response).to redirect_to(profil_path) }
it { expect(flash.alert).to eq(['Courriel invalide']) }
it { expect(flash.alert).to eq(["Le champ « Adresse éléctronique » est invalide. Saisir une adresse éléctronique valide, exemple : john.doe@exemple.fr"]) }
end
context 'when the user has an instructeur role' do

View file

@ -62,7 +62,7 @@ describe Users::SessionsController, type: :controller do
subject
expect(response).to render_template(:new)
expect(flash.alert).to eq('Courriel ou mot de passe incorrect.')
expect(flash.alert).to eq("Adresse éléctronique ou mot de passe incorrect.")
end
end
end

View file

@ -63,7 +63,7 @@ describe Champs::CnafChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Code postal doit posséder 5 caractères"])
expect(champ.errors.full_messages).to eq(["Le champ « Code postal » doit posséder 5 caractères"])
end
end
@ -72,7 +72,7 @@ describe Champs::CnafChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"])
expect(champ.errors.full_messages).to eq(["Le champ « Numero allocataire » doit être composé au maximum de 7 chiffres"])
end
end
@ -81,7 +81,7 @@ describe Champs::CnafChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"])
expect(champ.errors.full_messages).to eq(["Le champ « Numero allocataire » doit être composé au maximum de 7 chiffres"])
end
context 'and the validation_context is :brouillon' do
@ -96,7 +96,7 @@ describe Champs::CnafChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Code postal doit posséder 5 caractères"])
expect(champ.errors.full_messages).to eq(["Le champ « Code postal » doit posséder 5 caractères"])
end
end
end

View file

@ -63,7 +63,7 @@ describe Champs::DgfipChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Reference avis doit posséder 13 ou 14 caractères"])
expect(champ.errors.full_messages).to eq(["Le champ « Reference avis » doit posséder 13 ou 14 caractères"])
end
end
@ -72,7 +72,7 @@ describe Champs::DgfipChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Numero fiscal doit posséder 13 ou 14 caractères"])
expect(champ.errors.full_messages).to eq(["Le champ « Numero fiscal » doit posséder 13 ou 14 caractères"])
end
end
@ -81,7 +81,7 @@ describe Champs::DgfipChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Numero fiscal doit posséder 13 ou 14 caractères"])
expect(champ.errors.full_messages).to eq(["Le champ « Numero fiscal » doit posséder 13 ou 14 caractères"])
end
context 'and the validation_context is :brouillon' do
@ -96,7 +96,7 @@ describe Champs::DgfipChamp, type: :model do
it do
is_expected.to be false
expect(champ.errors.full_messages).to eq(["Reference avis doit posséder 13 ou 14 caractères"])
expect(champ.errors.full_messages).to eq(["Le champ « Reference avis » doit posséder 13 ou 14 caractères"])
end
end
end

View file

@ -31,7 +31,7 @@ describe Invite do
it do
expect(invite.save).to be false
expect(invite.errors.full_messages).to eq(["Email n'est pas valide"])
expect(invite.errors.full_messages).to eq(["Le champ « Email » n'est pas valide"])
end
context 'when an email is empty' do
@ -39,7 +39,7 @@ describe Invite do
it do
expect(invite.save).to be false
expect(invite.errors.full_messages).to eq(["Email doit être rempli"])
expect(invite.errors.full_messages).to eq(["Le champ « Email » doit être rempli"])
end
end
end

View file

@ -57,7 +57,7 @@ describe ProcedureRevision do
context 'when a libelle is missing' do
let(:tdc_params) { text_params.except(:libelle) }
it { expect(subject.errors.full_messages).to eq(["Libelle doit être rempli"]) }
it { expect(subject.errors.full_messages).to eq(["Le champ « Libelle » doit être rempli"]) }
end
context 'when a parent is incorrect' do

View file

@ -84,7 +84,7 @@ describe SuperAdmin, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le mot de passe est trop court"])
expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"])
end
end
@ -92,7 +92,7 @@ describe SuperAdmin, type: :model do
context 'when the password is long enough, but too simple' do
let(:password) { simple_password }
it { expect(subject).to eq(["Le mot de passe nest pas assez complexe"]) }
it { expect(subject).to eq(["Le champ « Mot de passe » nest pas assez complexe. Saisir un mot de passe plus complexe"]) }
end
end

View file

@ -143,7 +143,7 @@ describe TypeDeChamp do
it { is_expected.to be_invalid }
it do
subject.validate
expect(subject.errors.full_messages.to_sentence).to eq('Troll always invalid')
expect(subject.errors.full_messages.to_sentence).to eq("Le champ « Troll » always invalid")
end
end
end
@ -156,13 +156,13 @@ describe TypeDeChamp do
expect(type_de_champ.validate).to be_falsey
messages = type_de_champ.errors.full_messages
expect(messages.size).to eq(1)
expect(messages.first.starts_with?("#{type_de_champ.libelle} doit commencer par")).to be_truthy
expect(messages.first).to eq("Le champ « #{type_de_champ.libelle} » doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>")
type_de_champ.libelle = ''
expect(type_de_champ.validate).to be_falsey
messages = type_de_champ.errors.full_messages
expect(messages.size).to eq(2)
expect(messages.last.starts_with?("La liste doit commencer par")).to be_truthy
expect(messages.last).to eq("Le champ « La liste » doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>")
end
end

View file

@ -36,7 +36,7 @@ describe TypesDeChamp::LinkedDropDownListTypeDeChamp do
it { is_expected.to be_invalid }
it do
subject.validate
expect(subject.errors.full_messages).to eq ["#{subject.libelle} doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>"]
expect(subject.errors.full_messages).to eq ["Le champ « #{subject.libelle} » doit commencer par une entrée de menu primaire de la forme <code style='white-space: pre-wrap;'>--texte--</code>"]
end
end

View file

@ -378,7 +378,7 @@ describe User, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le mot de passe est trop court"])
expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"])
end
end
@ -386,7 +386,7 @@ describe User, type: :model do
context 'when the password is long enough, but too simple' do
let(:password) { simple_password }
it { expect(subject).to eq(["Le mot de passe nest pas assez complexe"]) }
it { expect(subject).to eq(["Le champ « Mot de passe » nest pas assez complexe. Saisir un mot de passe plus complexe"]) }
end
end
@ -404,7 +404,7 @@ describe User, type: :model do
let(:password) { 's' * (PASSWORD_MIN_LENGTH - 1) }
it 'reports an error about password length (but not about complexity)' do
expect(subject).to eq(["Le mot de passe est trop court"])
expect(subject).to eq(["Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"])
end
end

View file

@ -63,7 +63,7 @@ describe BillSignatureService do
let(:operations_hash) { [['1', 'hash1'], ['2', 'hash3']] }
it do
expect { subject }.to raise_error(/La validation a échoué : signature ne correspond pas à lempreinte/)
expect { subject }.to raise_error(/La validation a échoué : Le champ « signature » ne correspond pas à lempreinte/)
expect(BillSignature.count).to eq(0)
end
end

View file

@ -17,6 +17,6 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do
expect(page).to have_current_path(brouillon_dossier_path(dossier))
expect(page).to have_field(type_de_champ_text.libelle, with: text_value)
expect(page).to have_field(type_de_champ_phone.libelle, with: phone_value)
expect(page).to have_css('.field_with_errors', text: type_de_champ_phone.libelle)
expect(page).to have_css('label', text: type_de_champ_phone.libelle)
end
end

View file

@ -21,7 +21,7 @@ describe 'Creating a new procedure', js: true do
fill_in 'procedure_duree_conservation_dossiers_dans_ds', with: '3'
click_on 'Créer la démarche'
expect(page).to have_text('Libelle doit être rempli')
expect(page).to have_text('Le champ « Libelle » doit être rempli')
fill_in_dummy_procedure_details
click_on 'Créer la démarche'

View file

@ -282,7 +282,7 @@ describe 'fetch API Particulier Data', js: true do
wait_until { cnaf_champ.reload.code_postal == 'wrong_code' }
click_on 'Déposer le dossier'
expect(page).to have_content(/code postal doit posséder 5 caractères/)
expect(page).to have_content(/Le champ « Champs public code postal » doit posséder 5 caractères/)
VCR.use_cassette('api_particulier/success/composition_familiale') do
fill_in 'Le code postal', with: code_postal
@ -470,7 +470,7 @@ describe 'fetch API Particulier Data', js: true do
wait_until { dgfip_champ.reload.reference_avis == 'wrong_code' }
click_on 'Déposer le dossier'
expect(page).to have_content(/reference avis doit posséder 13 ou 14 caractères/)
expect(page).to have_content(/Le champ « Champs public reference avis » doit posséder 13 ou 14 caractères/)
VCR.use_cassette('api_particulier/success/avis_imposition') do
fill_in "La référence davis dimposition", with: reference_avis

View file

@ -5,7 +5,7 @@ describe 'Accessing the website in different languages:' do
scenario 'I can change the language of the page' do
visit new_user_session_path
expect(page).to have_text('Connectez-vous')
expect(page).to have_text("Connexion à #{APPLICATION_NAME}")
find('.fr-translate__btn').click
find('.fr-nav__link[hreflang="en"]').click

View file

@ -7,8 +7,8 @@ describe 'Signin in:' do
click_on 'Se connecter', match: :first
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)
expect(page).to have_content 'Adresse éléctronique ou mot de passe incorrect.'
expect(page).to have_field('Adresse éléctronique', with: user.email)
sign_in_with user.email, password
expect(page).to have_current_path dossiers_path

View file

@ -114,7 +114,7 @@ describe 'Creating a new dossier:' do
click_on 'Valider'
expect(page).to have_current_path(siret_dossier_path(dossier))
expect(page).to have_content('Le numéro SIRET doit comporter 14 chiffres')
expect(page).to have_content('Le champ « Siret » est invalide. Saisir un numéro SIRET avec 14 chiffres')
expect(page).to have_field('Numéro SIRET', with: '0000')
end
end

View file

@ -21,27 +21,27 @@ describe 'Signing up:' do
visit commencer_path(path: procedure.path)
click_on "Créer un compte #{APPLICATION_NAME}"
expect(page).to have_selector('.suspect-email', visible: false)
fill_in 'Email', with: 'bidou@yahoo.rf'
fill_in 'Adresse éléctronique', with: 'bidou@yahoo.rf'
fill_in 'Mot de passe', with: '12345'
end
scenario 'they can accept the suggestion', js: true do
expect(page).to have_selector('.suspect-email', visible: true)
click_on 'Oui'
expect(page).to have_field("Email", :with => 'bidou@yahoo.fr')
expect(page).to have_field("Adresse éléctronique", :with => 'bidou@yahoo.fr')
expect(page).to have_selector('.suspect-email', visible: false)
end
scenario 'they can discard the suggestion', js: true do
expect(page).to have_selector('.suspect-email', visible: true)
click_on 'Non'
expect(page).to have_field("Email", :with => 'bidou@yahoo.rf')
expect(page).to have_field("Adresse éléctronique", :with => 'bidou@yahoo.rf')
expect(page).to have_selector('.suspect-email', visible: false)
end
scenario 'they can fix the typo themselves', js: true do
expect(page).to have_selector('.suspect-email', visible: true)
fill_in 'Email', with: 'bidou@yahoo.fr'
fill_in 'Adresse éléctronique', with: 'bidou@yahoo.fr'
blur
expect(page).to have_selector('.suspect-email', visible: false)
end
@ -54,7 +54,7 @@ describe 'Signing up:' do
expect(page).to have_current_path new_user_registration_path
sign_up_with user_email, '1234567'
expect(page).to have_current_path user_registration_path
expect(page).to have_content 'Le mot de passe est trop court'
expect(page).to have_content "Le champ « Mot de passe » est trop court. Saisir un mot de passe avec au moins 8 caractères"
# Then with a good password
sign_up_with user_email, user_password

View file

@ -3,7 +3,7 @@ describe 'users/sessions/new.html.haml', type: :view do
before(:each) do
allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
allow(view).to receive(:resource).and_return(:user)
allow(view).to receive(:resource).and_return(User.new)
end
before do
@ -12,7 +12,7 @@ describe 'users/sessions/new.html.haml', type: :view do
end
it 'renders' do
expect(rendered).to have_field('Email')
expect(rendered).to have_field('Adresse éléctronique')
expect(rendered).to have_field('Mot de passe')
expect(rendered).to have_button('Se connecter')
end

View file

@ -1,143 +0,0 @@
.btn-fconnect {
all: initial;
color: #0b6ba8;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
background-color: #ffffff;
background-image: none;
border: 1px solid #ccc;
display: inline-block;
margin-bottom: 0;
line-height: 20px;
text-align: center;
text-shadow: 0 1px 1px rgba(255,255,255,0.75);
vertical-align: middle;
cursor: pointer;
border-color: rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);
}
.btn-fconnect-full {
font-size: 14px;
max-width: 175px;
padding: 11px 19px;
border-radius: 6px;
}
.btn-fconnect-mini {
font-size: 14px;
width: 182px;
padding: 11px 19px;
border-radius: 6px;
}
.btn-fconnect-full img {
width: 100%;
}
.btn-fconnect-mini img {
float:left;
width: 38px;
}
#fconnect-access {
all: initial;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
display: none;
position: absolute;
background: white;
border: 1px solid #ccc;
width: 300px;
padding: 15px;
margin-top: 20px;
z-index: 9990;
box-shadow: 1px 1px 3px #ccc;
}
#fconnect-access hr {
margin: 15px 0;
}
#fconnect-access:after, #fconnect-access:before {
bottom: 100%;
border: solid transparent;
content: "";
position: absolute;
}
#fconnect-access:after {
border-bottom-color: white;
border-width: 13px;
left: 10%;
}
#fconnect-access:before {
border-bottom-color: #ccc;
border-width: 14px;
left: 9.70%;
}
#fconnect-access .logout {
text-align: center;
margin-top: 15px;
}
#fconnect-access .btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
#fconnect-access .btn-default {
color: #333;
background-color: #fff;
border-color: #ccc;
}
#fconnect-access .btn-default:hover,
#fconnect-access .btn-default:focus {
color: #333;
background-color: #e6e6e6;
border-color: #adadad;
text-decoration: none;
}
#fc-background {
all: initial;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.80);
position: fixed;
top: 0;
left: 0;
z-index: 9999;
opacity: 0;
transition: opacity 0.2s ease-in;
}
#fc-background.fade-in {
opacity: 1;
}
#fc-background.fade-out {
opacity: 0;
}
#fconnect-iframe {
display: block;
width: 600px;
height: 500px;
margin: 60px auto 0 auto;
}