Merge pull request #6484 from betagouv/main

2021-09-22-01
This commit is contained in:
Paul Chavard 2021-09-23 10:07:39 +02:00 committed by GitHub
commit 92960b5ad0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 1400 additions and 460 deletions

View file

@ -0,0 +1,26 @@
@import "constants";
#sources-particulier-form {
h2 {
margin-bottom: 0;
}
h3 {
margin-top: 2 * $default-padding;
}
.explication {
padding: $default-padding;
ul {
list-style-type: circle;
list-style-position: inside;
padding-left: $default-padding;
margin-bottom: $default-padding;
}
}
.form input[type="checkbox"] {
margin-bottom: 0;
}
}

View file

@ -18,7 +18,7 @@ class Champs::SiretController < ApplicationController
begin
etablissement = find_etablissement_with_siret
rescue APIEntreprise::API::Error::RequestFailed, APIEntreprise::API::Error::ServiceUnavailable
# i18n-tasks-use t('errors.siret_network_error')
# i18n-tasks-use t('errors.messages.siret_network_error')
return siret_error(:network_error)
end
if etablissement.nil?

View file

@ -2,9 +2,12 @@ module DevisePopulatedResource
extend ActiveSupport::Concern
# During a GET /password/edit, the resource is a brand new object.
# This method gives access to the actual resource record, complete with email, relationships, etc.
# This method gives access to the actual resource record (if available), complete with email, relationships, etc.
#
# If the resource can't be found (typically because the reset password token has expired),
# returns the default blank record.
def populated_resource
resource_class.with_reset_password_token(resource.reset_password_token)
resource_class.with_reset_password_token(resource.reset_password_token) || resource
end
included do

View file

@ -11,7 +11,15 @@ module NewAdministrateur
def index
@procedure = procedure
@groupes_instructeurs = paginated_groupe_instructeurs
if procedure.routee?
@groupes_instructeurs = paginated_groupe_instructeurs
@instructeurs = []
@available_instructeur_emails = []
else
@groupes_instructeurs = []
@instructeurs = paginated_instructeurs
@available_instructeur_emails = available_instructeur_emails
end
end
def show
@ -131,15 +139,17 @@ module NewAdministrateur
else
if instructeurs.present?
instructeurs.each do |instructeur|
instructeur.assign_to_procedure(procedure)
end
procedure.defaut_groupe_instructeur.instructeurs << instructeurs
flash[:notice] = "Les instructeurs ont bien été affectés à la démarche"
end
end
end
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
if procedure.routee?
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
else
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
end
end
def remove_instructeur
@ -164,7 +174,12 @@ module NewAdministrateur
end
end
end
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
if procedure.routee?
redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur)
else
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
end
end
def update_routing_criteria_name
@ -174,6 +189,13 @@ module NewAdministrateur
notice: "Le libellé est maintenant « #{procedure.routing_criteria_name} »."
end
def update_routing_enabled
procedure.update!(routing_enabled: true)
redirect_to admin_procedure_groupe_instructeurs_path(procedure),
notice: "Le routage est activé."
end
def import
if !CSV_ACCEPTED_CONTENT_TYPES.include?(group_csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type)
flash[:alert] = "Importation impossible : veuillez importer un fichier CSV"
@ -227,7 +249,11 @@ module NewAdministrateur
end
def groupe_instructeur
procedure.groupe_instructeurs.find(params[:id])
if params[:id].present?
procedure.groupe_instructeurs.find(params[:id])
else
procedure.defaut_groupe_instructeur
end
end
def instructeur_id

View file

@ -0,0 +1,45 @@
module NewAdministrateur
class JetonParticulierController < AdministrateurController
before_action :retrieve_procedure
def api_particulier
end
def show
end
def update
@procedure.api_particulier_token = token
if @procedure.invalid?
flash.now.alert = @procedure.errors.full_messages
render :show
elsif scopes.empty?
flash.now.alert = t('.no_scopes_token')
render :show
else
@procedure.update!(api_particulier_scopes: scopes, api_particulier_sources: {})
redirect_to admin_procedure_api_particulier_sources_path(procedure_id: @procedure.id),
notice: t('.token_ok')
end
rescue APIParticulier::Error::Unauthorized
flash.now.alert = t('.not_found_token')
render :show
rescue APIParticulier::Error::HttpError
flash.now.alert = t('.network_error')
render :show
end
private
def scopes
@scopes ||= APIParticulier::API.new(token).scopes
end
def token
params[:procedure][:api_particulier_token]
end
end
end

View file

@ -0,0 +1,32 @@
module NewAdministrateur
class SourcesParticulierController < AdministrateurController
before_action :retrieve_procedure
def show
@available_sources = sources_service.available_sources
end
def update
if @procedure.update(api_particulier_sources: sources_params)
redirect_to admin_procedure_api_particulier_sources_path(@procedure), notice: t('.sources_ok')
else
flash.now.alert = @procedure.errors.full_messages
render :show
end
end
private
def sources_params
requested_sources = params
.with_defaults(api_particulier_sources: {})
.to_unsafe_hash[:api_particulier_sources]
sources_service.sanitize(requested_sources)
end
def sources_service
@sources_service ||= APIParticulier::Services::SourcesService.new(@procedure)
end
end
end

View file

@ -318,13 +318,13 @@ module Users
def show_demarche_en_test_banner
if @dossier.present? && @dossier.revision.draft?
flash.now.alert = t('.test_procedure')
flash.now.alert = t('users.dossiers.test_procedure')
end
end
def ensure_dossier_can_be_updated
if !dossier.can_be_updated_by_user?
flash.alert = t('.no_longer_editable')
flash.alert = t('users.dossiers.no_longer_editable')
redirect_to dossiers_path
end
end
@ -425,7 +425,7 @@ module Users
end
def forbidden!
flash[:alert] = t('.no_access')
flash[:alert] = t('users.dossiers.no_access')
redirect_to root_path
end

View file

@ -5,6 +5,7 @@ module Users
if: -> { instructeur_signed_in? }
def show
@waiting_transfers = current_user.dossiers.joins(:transfer).group('dossier_transfers.email').count.to_a
end
def renew_api_token
@ -27,6 +28,12 @@ module Users
redirect_to profil_path
end
def transfer_all_dossiers
DossierTransfer.initiate(next_owner_email, current_user.dossiers)
flash.notice = t('.new_transfer', count: current_user.dossiers.count, email: next_owner_email)
redirect_to profil_path
end
private
def update_email_params
@ -40,5 +47,9 @@ module Users
def redirect_if_instructeur
redirect_to profil_path
end
def next_owner_email
params[:next_owner]
end
end
end

View file

@ -0,0 +1,34 @@
class APIParticulier::API
include APIParticulier::Error
INTROSPECT_RESOURCE_NAME = "introspect"
TIMEOUT = 20
def initialize(token)
@token = token
end
def scopes
get(INTROSPECT_RESOURCE_NAME)[:scopes]
end
private
def get(resource_name, params = {})
url = [API_PARTICULIER_URL, resource_name].join("/")
response = Typhoeus.get(url,
headers: { accept: "application/json", "X-API-Key": @token },
params: params,
timeout: TIMEOUT)
if response.success?
JSON.parse(response.body, symbolize_names: true)
elsif response.code == 401
raise Unauthorized.new(response)
else
raise RequestFailed.new(response)
end
end
end

View file

@ -0,0 +1,32 @@
module APIParticulier
module Error
class HttpError < ::StandardError
def initialize(response)
connect_time = response.connect_time
curl_message = response.return_message
http_error_code = response.code
datetime = response.headers.fetch('Date', DateTime.current.inspect)
total_time = response.total_time
uri = URI.parse(response.effective_url)
url = "#{uri.host}#{uri.path}"
msg = <<~TEXT
url: #{url}
HTTP error code: #{http_error_code}
#{response.body}
curl message: #{curl_message}
total time: #{total_time}
connect time: #{connect_time}
datetime: #{datetime}
TEXT
super(msg)
end
end
class RequestFailed < HttpError; end
class Unauthorized < HttpError; end
end
end

View file

@ -0,0 +1,64 @@
module APIParticulier
module Services
class SourcesService
def initialize(procedure)
@procedure = procedure
end
def available_sources
@procedure.api_particulier_scopes
.map { |provider_and_scope| raw_scopes[provider_and_scope] }
.map { |provider, scope| extract_sources(provider, scope) }
.reduce({}) { |acc, el| acc.deep_merge(el) }
end
# Remove sources not available for the procedure
def sanitize(requested_sources)
requested_sources_a = h_to_a(requested_sources)
available_sources_a = h_to_a(available_sources)
filtered_sources_a = requested_sources_a.intersection(available_sources_a)
a_to_h(filtered_sources_a)
end
private
# { 'cnaf' => { 'scope' => ['a', 'b'] }} => [['cnaf', 'scope', 'a'], ['cnaf', 'scope', 'b']]
def h_to_a(h)
h.reduce([]) { |acc, (provider, scopes)| scopes.each { |scope, values| values.each { |s, _| acc << [provider, scope, s] } }; acc }
end
# [['cnaf', 'scope', 'a'], ['cnaf', 'scope', 'b']] => { 'cnaf' => { 'scope' => ['a', 'b'] }}
def a_to_h(a)
h = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
a.reduce(h) { |acc, (provider, scope, source)| h[provider][scope] << source; acc }
end
def extract_sources(provider, scope)
{ provider => { scope => providers[provider][scope] } }
end
def raw_scopes
{
'cnaf_allocataires' => ['cnaf', 'allocataires'],
'cnaf_enfants' => ['cnaf', 'enfants'],
'cnaf_adresse' => ['cnaf', 'adresse'],
'cnaf_quotient_familial' => ['cnaf', 'quotient_familial']
}
end
def providers
{
'cnaf' => {
'allocataires' => ['noms_prenoms', 'date_de_naissance', 'sexe'],
'enfants' => ['noms_prenoms', 'date_de_naissance', 'sexe'],
'adresse' => ['identite', 'complement_d_identite', 'complement_d_identite_geo', 'numero_et_rue', 'lieu_dit', 'code_postal_et_ville', 'pays'],
'quotient_familial' => ['quotient_familial', 'annee', 'mois']
}
}
end
end
end
end

View file

@ -3,22 +3,22 @@ class Helpscout::FormAdapter
def self.options
[
[I18n.t(TYPE_INFO, scope: [:support, :question]), TYPE_INFO, FAQ_CONTACTER_SERVICE_EN_CHARGE_URL],
[I18n.t(TYPE_PERDU, scope: [:support, :question]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL],
[I18n.t(TYPE_INSTRUCTION, scope: [:support, :question]), TYPE_INSTRUCTION, FAQ_OU_EN_EST_MON_DOSSIER_URL],
[I18n.t(TYPE_AMELIORATION, scope: [:support, :question]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL],
[I18n.t(TYPE_AUTRE, scope: [:support, :question]), TYPE_AUTRE]
[I18n.t(:question, scope: [:support, :index, TYPE_INFO]), TYPE_INFO, FAQ_CONTACTER_SERVICE_EN_CHARGE_URL],
[I18n.t(:question, scope: [:support, :index, TYPE_PERDU]), TYPE_PERDU, LISTE_DES_DEMARCHES_URL],
[I18n.t(:question, scope: [:support, :index, TYPE_INSTRUCTION]), TYPE_INSTRUCTION, FAQ_OU_EN_EST_MON_DOSSIER_URL],
[I18n.t(:question, scope: [:support, :index, TYPE_AMELIORATION]), TYPE_AMELIORATION, FEATURE_UPVOTE_URL],
[I18n.t(:question, scope: [:support, :index, TYPE_AUTRE]), TYPE_AUTRE]
]
end
def self.admin_options
[
[I18n.t(ADMIN_TYPE_QUESTION, scope: [:supportadmin], app_name: APPLICATION_NAME), ADMIN_TYPE_QUESTION],
[I18n.t(ADMIN_TYPE_RDV, scope: [:supportadmin], app_name: APPLICATION_NAME), ADMIN_TYPE_RDV],
[I18n.t(ADMIN_TYPE_SOUCIS, scope: [:supportadmin], app_name: APPLICATION_NAME), ADMIN_TYPE_SOUCIS],
[I18n.t(ADMIN_TYPE_PRODUIT, scope: [:supportadmin]), ADMIN_TYPE_PRODUIT],
[I18n.t(ADMIN_TYPE_DEMANDE_COMPTE, scope: [:supportadmin]), ADMIN_TYPE_DEMANDE_COMPTE],
[I18n.t(ADMIN_TYPE_AUTRE, scope: [:supportadmin]), ADMIN_TYPE_AUTRE]
[I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_QUESTION], app_name: APPLICATION_NAME), ADMIN_TYPE_QUESTION],
[I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_RDV], app_name: APPLICATION_NAME), ADMIN_TYPE_RDV],
[I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_SOUCIS], app_name: APPLICATION_NAME), ADMIN_TYPE_SOUCIS],
[I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_PRODUIT]), ADMIN_TYPE_PRODUIT],
[I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_DEMANDE_COMPTE]), ADMIN_TYPE_DEMANDE_COMPTE],
[I18n.t(:question, scope: [:support, :admin, ADMIN_TYPE_AUTRE]), ADMIN_TYPE_AUTRE]
]
end

View file

@ -170,6 +170,8 @@ class DossierMailer < ApplicationMailer
# This is an override of `default_i18n_subject` method
# https://api.rubyonrails.org/v5.0.0/classes/ActionMailer/Base.html#method-i-default_i18n_subject
#
# i18n-tasks-use t("dossier_mailer.#{action}.subject")
def default_i18n_subject(interpolations = {})
if interpolations[:state]
mailer_scope = self.class.mailer_name.tr('/', '.')

View file

@ -14,6 +14,7 @@
class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern
include FileValidationConcern
belongs_to :procedure, optional: false
@ -21,8 +22,10 @@ class AttestationTemplate < ApplicationRecord
has_one_attached :signature
validates :footer, length: { maximum: 190 }
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: 1.megabytes }
validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: 1.megabytes }
FILE_MAX_SIZE = 1.megabytes
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: file_size_validation(FILE_MAX_SIZE)
validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: file_size_validation(FILE_MAX_SIZE)
DOSSIER_STATE = Dossier.states.fetch(:accepte)

View file

@ -17,6 +17,7 @@
#
class Avis < ApplicationRecord
include EmailSanitizableConcern
include FileValidationConcern
belongs_to :dossier, inverse_of: :avis, touch: true, optional: false
belongs_to :experts_procedure, optional: false
@ -27,19 +28,19 @@ class Avis < ApplicationRecord
has_one :expert, through: :experts_procedure
has_one :procedure, through: :experts_procedure
FILE_MAX_SIZE = 20.megabytes
validates :piece_justificative_file,
content_type: AUTHORIZED_CONTENT_TYPES,
size: { less_than: 20.megabytes }
size: file_size_validation(FILE_MAX_SIZE)
validates :introduction_file,
content_type: AUTHORIZED_CONTENT_TYPES,
size: { less_than: 20.megabytes }
size: file_size_validation(FILE_MAX_SIZE)
validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, allow_nil: true
validates :claimant, presence: true
validates :piece_justificative_file, size: { less_than: 20.megabytes }
validates :introduction_file, size: { less_than: 20.megabytes }
validates :piece_justificative_file, size: file_size_validation(FILE_MAX_SIZE)
validates :introduction_file, size: file_size_validation(FILE_MAX_SIZE)
before_validation -> { sanitize_email(:email) }
default_scope { joins(:dossier) }

View file

@ -18,10 +18,11 @@
# type_de_champ_id :integer
#
class Champs::PieceJustificativeChamp < Champ
MAX_SIZE = 200.megabytes
include FileValidationConcern
FILE_MAX_SIZE = 200.megabytes
validates :piece_justificative_file,
size: { less_than: MAX_SIZE },
size: file_size_validation(FILE_MAX_SIZE),
if: -> { !type_de_champ.skip_pj_validation }
validates :piece_justificative_file,

View file

@ -18,7 +18,8 @@
# type_de_champ_id :integer
#
class Champs::TitreIdentiteChamp < Champ
MAX_SIZE = 20.megabytes
include FileValidationConcern
FILE_MAX_SIZE = 20.megabytes
ACCEPTED_FORMATS = [
"image/png",
@ -30,7 +31,7 @@ class Champs::TitreIdentiteChamp < Champ
#
validates :piece_justificative_file,
content_type: ACCEPTED_FORMATS,
size: { less_than: MAX_SIZE }
size: file_size_validation(FILE_MAX_SIZE)
def main_value_name
:piece_justificative_file

View file

@ -12,6 +12,8 @@
# instructeur_id :bigint
#
class Commentaire < ApplicationRecord
include FileValidationConcern
self.ignored_columns = [:user_id]
belongs_to :dossier, inverse_of: :commentaires, touch: true, optional: false
@ -24,9 +26,10 @@ class Commentaire < ApplicationRecord
validates :body, presence: { message: "ne peut être vide" }
FILE_MAX_SIZE = 20.megabytes
validates :piece_jointe,
content_type: AUTHORIZED_CONTENT_TYPES,
size: { less_than: 20.megabytes }
size: file_size_validation(FILE_MAX_SIZE)
default_scope { order(created_at: :asc) }
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) }

View file

@ -0,0 +1,8 @@
module FileValidationConcern
extend ActiveSupport::Concern
class_methods do
def file_size_validation(file_max_size = 200.megabytes)
{ less_than: file_max_size, message: I18n.t('errors.messages.file_size_out_of_range', file_size_limit: ActiveSupport::NumberHelper.number_to_human_size(file_max_size)) }
end
end
end

View file

@ -73,19 +73,15 @@ class Instructeur < ApplicationRecord
end
def assign_to_procedure(procedure)
begin
assign_to.create({
procedure: procedure,
groupe_instructeur: procedure.defaut_groupe_instructeur
})
true
rescue ActiveRecord::RecordNotUnique
false
if !procedure.defaut_groupe_instructeur.in?(groupe_instructeurs)
groupe_instructeurs << procedure.defaut_groupe_instructeur
end
end
def remove_from_procedure(procedure)
!!(procedure.defaut_groupe_instructeur.in?(groupe_instructeurs) && groupe_instructeurs.destroy(procedure.defaut_groupe_instructeur))
if procedure.defaut_groupe_instructeur.in?(groupe_instructeurs)
groupe_instructeurs.destroy(procedure.defaut_groupe_instructeur)
end
end
def last_week_overview

View file

@ -6,6 +6,8 @@
# aasm_state :string default("brouillon")
# allow_expert_review :boolean default(TRUE), not null
# api_entreprise_token :string
# api_particulier_scopes :text default([]), is an Array
# api_particulier_sources :jsonb
# ask_birthday :boolean default(FALSE), not null
# auto_archive_on :date
# cadre_juridique :string
@ -33,6 +35,7 @@
# path :string not null
# published_at :datetime
# routing_criteria_name :text default("Votre ville")
# routing_enabled :boolean
# test_started_at :datetime
# unpublished_at :datetime
# web_hook_url :string
@ -49,6 +52,7 @@
class Procedure < ApplicationRecord
include ProcedureStatsConcern
include EncryptableConcern
include FileValidationConcern
include Discard::Model
self.discard_column = :hidden_at
@ -237,6 +241,8 @@ class Procedure < ApplicationRecord
validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }
validates :duree_conservation_dossiers_hors_ds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_with MonAvisEmbedValidator
FILE_MAX_SIZE = 20.megabytes
validates :notice, content_type: [
"application/msword",
"application/pdf",
@ -249,7 +255,7 @@ class Procedure < ApplicationRecord
"image/jpg",
"image/png",
"text/plain"
], size: { less_than: 20.megabytes }, if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
], size: file_size_validation(FILE_MAX_SIZE), if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
validates :deliberation, content_type: [
"application/msword",
@ -260,14 +266,15 @@ class Procedure < ApplicationRecord
"image/jpg",
"image/png",
"text/plain"
], size: { less_than: 20.megabytes }, if: -> { new_record? || created_at > Date.new(2020, 4, 29) }
], size: file_size_validation(FILE_MAX_SIZE), if: -> { new_record? || created_at > Date.new(2020, 4, 29) }
LOGO_MAX_SIZE = 5.megabytes
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
size: { less_than: 5.megabytes },
size: file_size_validation(LOGO_MAX_SIZE),
if: -> { new_record? || created_at > Date.new(2020, 11, 13) }
validates :api_entreprise_token, jwt_token: true, allow_blank: true
validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/, message: "n'est pas un jeton valide" }, allow_blank: true
validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/ }, allow_blank: true
before_save :update_juridique_required
after_initialize :ensure_path_exists
@ -447,6 +454,7 @@ class Procedure < ApplicationRecord
procedure.administrateurs = [admin]
procedure.api_entreprise_token = nil
procedure.encrypted_api_particulier_token = nil
procedure.api_particulier_scopes = []
else
procedure.administrateurs = administrateurs
end
@ -625,7 +633,7 @@ class Procedure < ApplicationRecord
end
def routee?
groupe_instructeurs.size > 1
routing_enabled? || groupe_instructeurs.size > 1
end
def defaut_groupe_instructeur_for_new_dossier

View file

@ -48,14 +48,13 @@
- if matching_archive.status == 'generated' && matching_archive.file.attached?
= link_to url_for(matching_archive.file), class: 'button primary' do
%span.icon.download-white
= t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure])
= t(:archive_ready_html, scope: [:instructeurs, :procedure], generated_period: time_ago_in_words(matching_archive.updated_at))
- else
%span.icon.retry
= t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure])
= t(:archive_pending_html, scope: [:instructeurs, :procedure], created_period: time_ago_in_words(matching_archive.created_at))
- elsif weight < 1.gigabyte
= link_to instructeur_archives_path(@procedure, type:'monthly', month: month.strftime('%Y-%m')), method: :post, class: "button" do
%span.icon.new-folder
Demander la création
- else
Archive trop volumineuse

View file

@ -25,7 +25,7 @@ as defined by the routes in the `admin/` namespace
<%= link_to "Delayed Jobs", manager_delayed_job_path, class: "navigation__link" %>
<%= link_to "Features", manager_flipper_path, class: "navigation__link" %>
<% if Rails.env.production? %>
<% if Rails.env.production? && ENV['SENDINBLUE_ENABLED'] == 'enabled'%>
<%= link_to "Sendinblue", ENV.fetch("SENDINBLUE_LOGIN_URL"), class: "navigation__link", target: '_blank' %>
<% end %>
</nav>

View file

@ -0,0 +1,52 @@
.card
= form_for procedure,
url: { action: :update_routing_criteria_name },
html: { class: 'form' } do |f|
= f.label :routing_criteria_name do
Libellé du routage
%p.notice Ce texte apparaitra sur le formulaire usager comme le libellé dune liste
= f.text_field :routing_criteria_name, placeholder: 'ex. Votre ville', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des Groupes
= form_for :groupe_instructeur, html: { class: 'form' } do |f|
= f.label :label do
Ajouter un groupe
%p.notice Ce groupe sera un choix de la liste « #{procedure.routing_criteria_name} » .
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
- csv_max_size = NewAdministrateur::GroupeInstructeursController::CSV_MAX_SIZE
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t(".existing_groupe", count: groupes_instructeurs.total_count)
%tbody
- groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= link_to "voir", admin_procedure_groupe_instructeur_path(procedure, group)
- if groupes_instructeurs.many?
- if group.dossiers.empty?
%td.actions
= link_to admin_procedure_groupe_instructeur_path(procedure, group), { method: :delete, class: 'button', data: { confirm: "Êtes-vous sûr de vouloir supprimer le groupe « #{group.label} » ?" }} do
%span.icon.delete
supprimer ce groupe
- else
%td.actions
= link_to reaffecter_dossiers_admin_procedure_groupe_instructeur_path(procedure, group), class: 'button', title:'Réaffecter les dossiers à un autre groupe afin de pouvoir le supprimer' do
%span.icon.follow
déplacer les dossiers
= paginate groupes_instructeurs

View file

@ -0,0 +1,37 @@
.card
.card-title Affectation des instructeurs
= form_for :instructeur, url: { action: :add_instructeur, id: groupe_instructeur.id }, html: { class: 'form' } do |f|
.instructeur-wrapper
- if !procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: available_instructeur_emails, selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'email instructeur',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t('.assigned_instructeur', count: instructeurs.count)
%tbody
- instructeurs.each do |instructeur|
%tr
%td
%span.icon.person
#{instructeur.email}
- confirmation_message = procedure.routee? ? "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » du groupe « #{groupe_instructeur.label} » ?" : "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » de la démarche ?"
%td.actions= button_to 'retirer',
{ action: :remove_instructeur, id: groupe_instructeur.id },
{ method: :delete,
data: { confirm: confirmation_message },
params: { instructeur: { id: instructeur.id }},
class: 'button' }
= paginate instructeurs

View file

@ -0,0 +1,5 @@
.card
.card-title Routage
%p.notice= t('.notice_html')
= link_to 'Activer le routage', update_routing_enabled_admin_procedure_groupe_instructeurs_path(procedure), class: 'button primary', method: 'patch'

View file

@ -1,57 +1,22 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Groupes dinstructeurs'] }
- if @procedure.routee?
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Groupes dinstructeurs'] }
- else
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Instructeurs'] }
.container.groupe-instructeur
.card
= form_for @procedure,
url: { action: :update_routing_criteria_name },
html: { class: 'form' } do |f|
- if @procedure.routee?
= render partial: 'new_administrateur/groupe_instructeurs/edit', locals: { procedure: @procedure, groupes_instructeurs: @groupes_instructeurs }
- else
= render partial: 'new_administrateur/groupe_instructeurs/routing', locals: { procedure: @procedure }
= render partial: 'new_administrateur/groupe_instructeurs/instructeurs',
locals: { procedure: @procedure,
groupe_instructeur: @procedure.defaut_groupe_instructeur,
instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails }
= f.label :routing_criteria_name do
Libellé du routage
%p.notice Ce texte apparaitra sur le formulaire usager comme le libellé dune liste
= f.text_field :routing_criteria_name, placeholder: 'ex. Votre ville', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des Groupes
= form_for :groupe_instructeur, html: { class: 'form' } do |f|
= f.label :label do
Ajouter un groupe
%p.notice Ce groupe sera un choix de la liste « #{@procedure.routing_criteria_name} » .
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
- csv_max_size = NewAdministrateur::GroupeInstructeursController::CSV_MAX_SIZE
= form_tag import_admin_procedure_groupe_instructeurs_path(@procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t(".existing_groupe", count: @groupes_instructeurs.total_count)
%tbody
- @groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= link_to "voir", admin_procedure_groupe_instructeur_path(@procedure, group)
- if @groupes_instructeurs.many?
- if group.dossiers.empty?
%td.actions
= link_to admin_procedure_groupe_instructeur_path(@procedure, group), { method: :delete, class: 'button', data: { confirm: "Êtes-vous sûr de vouloir supprimer le groupe « #{group.label} » ?" }} do
%span.icon.delete
supprimer ce groupe
- else
%td.actions
= link_to reaffecter_dossiers_admin_procedure_groupe_instructeur_path(@procedure, group), class: 'button', title:'Réaffecter les dossiers à un autre groupe afin de pouvoir le supprimer' do
%span.icon.follow
déplacer les dossiers
= paginate @groupes_instructeurs

View file

@ -1,55 +1,14 @@
- if feature_enabled?(:administrateur_routage)
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to('Groupes dinstructeurs', admin_procedure_groupe_instructeurs_path(@procedure)),
@groupe_instructeur.label] }
- else
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Instructeurs'] }
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to('Groupes dinstructeurs', admin_procedure_groupe_instructeurs_path(@procedure)),
@groupe_instructeur.label] }
.container.groupe-instructeur
- if feature_enabled?(:administrateur_routage)
= render partial: 'new_administrateur/groups_header'
.card
.card-title Affectation des instructeurs
= form_for :instructeur,
url: { action: :add_instructeur },
html: { class: 'form' } do |f|
.instructeur-wrapper
- if !@procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
- hidden_field_id = SecureRandom.uuid
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
= react_component("ComboMultipleDropdownList",
options: @available_instructeur_emails, selected: [], disabled: [],
hiddenFieldId: hidden_field_id,
label: 'email instructeur',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 }= t('.assigned_instructeur', count: @instructeurs.count)
%tbody
- @instructeurs.each do |instructeur|
%tr
%td
%span.icon.person
#{instructeur.email}
%td.actions= button_to 'retirer',
{ action: :remove_instructeur },
{ method: :delete,
data: { confirm: feature_enabled?(:administrateur_routage) ? "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » du groupe  « #{@groupe_instructeur.label} » ?" : "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » de la démarche ?" },
params: { instructeur: { id: instructeur.id }},
class: 'button' }
= paginate @instructeurs
= render partial: 'new_administrateur/groups_header'
= render partial: 'new_administrateur/groupe_instructeurs/instructeurs',
locals: { procedure: @procedure,
groupe_instructeur: @groupe_instructeur,
instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails }

View file

@ -0,0 +1,34 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
Procedure.human_attribute_name(:jeton_api_particulier)] }
.container
.flex
= link_to admin_procedure_api_particulier_jeton_path, class: 'card-admin' do
- if @procedure.api_particulier_token.blank?
%div
%span.icon.clock
%p.card-admin-status-todo= t('.needs_configuration')
- else
%div
%span.icon.accept
%p.card-admin-status-accept= t('.already_configured')
%div
%p.card-admin-title
= Procedure.human_attribute_name(:jeton_api_particulier)
%p.button= t('views.shared.actions.edit')
- if @procedure.api_particulier_scopes.present?
= link_to admin_procedure_api_particulier_sources_path, class: 'card-admin' do
- if @procedure.api_particulier_token.blank?
%div
%span.icon.clock
%p.card-admin-status-todo= t('.needs_configuration')
- else
%div
%span.icon.accept
%p.card-admin-status-accept= t('.already_configured')
%div
%p.card-admin-title= t('new_administrateur.sources_particulier.show.data_sources')
%p.button= t('views.shared.actions.edit')

View file

@ -0,0 +1,22 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to(Procedure.human_attribute_name(:jeton_api_particulier), admin_procedure_api_particulier_path(@procedure)),
'Jeton'] }
.container
%h1.page-title
= t('.configure_token')
.container
%h1
= form_with model: @procedure, url: admin_procedure_api_particulier_jeton_path, local: true, html: { class: 'form' } do |f|
%p.explication
= t('.api_particulier_description_html', app_name: APPLICATION_NAME)
= f.label :api_particulier_token
.desc.mb-2
%p= t('.token_description')
= f.password_field :api_particulier_token, class: 'form-control', required: :required
.text-right
= f.button t('views.shared.actions.save'), class: 'button primary send'

View file

@ -18,7 +18,7 @@
.admin-procedures-list-row.actions.flex.justify-between
%div
- if feature_enabled?(:administrateur_routage)
- if procedure.routee?
%span.icon.person
%span.badge.baseline= procedure.groupe_instructeurs.count
- else

View file

@ -16,33 +16,39 @@
%li.mb-1= t("update_description#{postfix}", label: change[:label], to: change[:to], scope: [:new_administrateur, :revision_changes])
- when :mandatory
- if change[:from] == false
%li.mb-1= t(:enabled, label: change[:label], scope: [:new_administrateur, :revision_changes, "update_mandatory#{postfix}"])
-# i18n-tasks-use t('new_administrateur.revision_changes.update_mandatory.enabled')
-# i18n-tasks-use t('new_administrateur.revision_changes.update_mandatory_private.enabled')
%li.mb-1= t("new_administrateur.revision_changes.update_mandatory#{postfix}.enabled", label: change[:label])
- else
%li.mb-1= t(:disabled, label: change[:label], scope: [:new_administrateur, :revision_changes, "update_mandatory#{postfix}"])
-# i18n-tasks-use t('new_administrateur.revision_changes.update_mandatory.disabled')
-# i18n-tasks-use t('new_administrateur.revision_changes.update_mandatory_private.disabled')
%li.mb-1= t("new_administrateur.revision_changes.update_mandatory#{postfix}.disabled", label: change[:label])
- when :piece_justificative_template
%li.mb-1= t("update_piece_justificative_template#{postfix}", label: change[:label], scope: [:new_administrateur, :revision_changes])
-# i18n-tasks-use t('new_administrateur.revision_changes.update_piece_justificative_template')
-# i18n-tasks-use t('new_administrateur.revision_changes.update_piece_justificative_template_private')
%li.mb-1= t("new_administrateur.revision_changes.update_piece_justificative_template#{postfix}", label: change[:label])
- when :drop_down_options
- added = change[:to].sort - change[:from].sort
- removed = change[:from].sort - change[:to].sort
%li.mb-1
= t("update_drop_down_options#{postfix}", label: change[:label], scope: [:new_administrateur, :revision_changes])
= t("update_drop_down_options#{postfix}", scope: [:new_administrateur, :revision_changes], label: change[:label])
%ul
- if added.present?
%li= t(:add_option, items: added.map{ |term| "« #{term.strip} »" }.join(", "), scope: [:new_administrateur, :revision_changes])
%li= t(:add_option, scope: [:new_administrateur, :revision_changes], items: added.map{ |term| "« #{term.strip} »" }.join(", "))
- if removed.present?
%li= t(:remove_option, items: removed.map{ |term| "« #{term.strip} »" }.join(", "), scope: [:new_administrateur, :revision_changes])
%li= t(:remove_option, scope: [:new_administrateur, :revision_changes], items: removed.map{ |term| "« #{term.strip} »" }.join(", "))
- when :carte_layers
- added = change[:to].sort - change[:from].sort
- removed = change[:from].sort - change[:to].sort
%li.mb-1
= t("update_carte_layers#{postfix}", label: change[:label], scope: [:new_administrateur, :revision_changes])
= t("update_carte_layers#{postfix}", scope: [:new_administrateur, :revision_changes], label: change[:label])
%ul
- if added.present?
%li= t(:add_option, items: added.map{ |term| "« #{t(term, scope: [:new_administrateur, :carte_layers])} »" }.join(", "), scope: [:new_administrateur, :revision_changes])
%li= t(:add_option, scope: [:new_administrateur, :revision_changes], items: added.map{ |term| "« #{t(term, scope: [:new_administrateur, :carte_layers])} »" }.join(", "))
- if removed.present?
%li= t(:remove_option, items: removed.map{ |term| "« #{t(term, scope: [:new_administrateur, :carte_layers])} »" }.join(", "), scope: [:new_administrateur, :revision_changes])
%li= t(:remove_option, scope: [:new_administrateur, :revision_changes], items: removed.map{ |term| "« #{t(term, scope: [:new_administrateur, :carte_layers])} »" }.join(", "))
- move_changes, move_private_changes = changes.filter { |change| change[:op] == :move }.partition { |change| !change[:private] }
- if move_changes.size != 0
%li.mb-1= t(:move, count: move_changes.size, scope: [:new_administrateur, :revision_changes])
%li.mb-1= t(:move, scope: [:new_administrateur, :revision_changes], count: move_changes.size)
- if move_private_changes.size != 0
%li.mb-1= t(:move_private, count: move_private_changes.size, scope: [:new_administrateur, :revision_changes])
%li.mb-1= t(:move_private, scope: [:new_administrateur, :revision_changes], count: move_private_changes.size)

View file

@ -107,14 +107,8 @@
%p.card-admin-subtitle Gestion de la démarche
%p.button Modifier
- if feature_enabled?(:administrateur_routage)
- instructeur_link = admin_procedure_groupe_instructeurs_path(@procedure)
- else
- instructeur_link = admin_procedure_groupe_instructeur_path(@procedure, @procedure.defaut_groupe_instructeur)
= link_to instructeur_link, id: 'groupe-instructeurs', class: 'card-admin' do
- if feature_enabled?(:administrateur_routage) || @procedure.instructeurs.count > 1
= link_to admin_procedure_groupe_instructeurs_path(@procedure), id: 'groupe-instructeurs', class: 'card-admin' do
- if @procedure.routee? || @procedure.instructeurs.count > 1
%div
%span.icon.accept
%p.card-admin-status-accept Validé
@ -124,12 +118,12 @@
%p.card-admin-status-todo À faire
%div
%p.card-admin-title
- if feature_enabled?(:administrateur_routage)
- if @procedure.routee?
%span.badge.baseline= @procedure.groupe_instructeurs.count
- else
%span.badge.baseline= @procedure.instructeurs.count
= feature_enabled?(:administrateur_routage) ? "Groupe Instructeurs" : "#{"Instructeur".pluralize(@procedure.instructeurs.count)}"
= @procedure.routee? ? "Groupe Instructeurs" : "#{"Instructeur".pluralize(@procedure.instructeurs.count)}"
%p.card-admin-subtitle Suivi des dossiers
%p.button Modifier
@ -194,10 +188,25 @@
%span.icon.clock
%p.card-admin-status-todo À configurer
%div
%p.card-admin-title Jeton
%p.card-admin-title Jeton Entreprise
%p.card-admin-subtitle Configurer le jeton API entreprise
%p.button Modifier
- if feature_enabled?(:api_particulier)
= link_to admin_procedure_api_particulier_path(@procedure), class: 'card-admin' do
- if @procedure.api_particulier_token.present?
%div
%span.icon.accept
%p.card-admin-status-accept= t('.ready')
- else
%div
%span.icon.clock
%p.card-admin-status-todo= t('.needs_configuration')
%div
%p.card-admin-title= Procedure.human_attribute_name(:api_particulier_token)
%p.card-admin-subtitle= t('.configure_api_particulier_token')
%p.button= t('views.shared.actions.edit')
= link_to monavis_admin_procedure_path(@procedure), class: 'card-admin' do
- if @procedure.monavis_embed.present?
%div

View file

@ -0,0 +1,28 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to(Procedure.human_attribute_name(:jeton_api_particulier), admin_procedure_api_particulier_path(@procedure)),
t('.data_sources')] }
.container
%h1.page-title= t('.title')
.container#sources-particulier-form.mb-2
= form_with model: @procedure, url: admin_procedure_api_particulier_sources_path, local: true, html: { class: 'form' } do |f|
.explication= t('.explication_html')
- @available_sources.each do |provider_key, scopes|
%h2.header-section= t("api_particulier.providers.#{provider_key}.libelle")
- scopes.each do |scope_key, sources|
%h3.explication-libelle= t("api_particulier.providers.#{provider_key}.scopes.#{scope_key}.libelle")
%ul.procedure-admin-api-particulier-sources
- sources.each do |source_key, enabled_hash|
- enabled = (@procedure.api_particulier_sources.dig(provider_key, scope_key)&.include?(source_key)).present?
%li
%label
= check_box_tag "api_particulier_sources[#{provider_key}][#{scope_key}][]", "#{source_key}", enabled
#{t("api_particulier.providers.#{provider_key}.scopes.#{scope_key}.#{source_key}")}
.text-right
= f.button t('views.shared.actions.save'), class: 'button primary send'

View file

@ -8,7 +8,6 @@
- when :network_error
= t('errors.messages.siret_network_error')
-# i18n-tasks-use t('errors.messages.siret_network_error')
- else
- if siret.present? && siret == etablissement&.siret

View file

@ -2,7 +2,7 @@
%tbody
- if etablissement.diffusable_commercialement == false && profile != 'instructeur'
%tr
%td= t('warning_for_private_info', etablissement: raison_sociale_or_name(etablissement), scope: 'views.shared.dossiers.identite_entreprise')
%td= t('warning_for_private_info', scope: 'views.shared.dossiers.identite_entreprise', etablissement: raison_sociale_or_name(etablissement))
- else
%tr
%th.libelle Dénomination :

View file

@ -3,8 +3,8 @@
Sélectionnez une des valeurs
%label
= form.radio_button :value, Individual::GENDER_FEMALE
= t(Individual::GENDER_FEMALE, scope: 'activerecord.attributes.individual.gender_options')
= Individual.human_attribute_name('gender.female')
%label
= form.radio_button :value, Individual::GENDER_MALE
= t('activerecord.attributes.individual.gender_options')[Individual::GENDER_MALE.to_sym] # GENDER_MALE contains a point letter
= Individual.human_attribute_name('gender.male')

View file

@ -3,10 +3,10 @@
#contact-form
.container
%h1.new-h1
= t('contact_team', scope: [:supportadmin])
= t('.contact_team')
.description
= t('admin_intro_html', scope: [:supportadmin], contact_path: contact_path)
= t('.admin_intro_html', contact_path: contact_path)
%br
%p.mandatory-explanation= t('asterisk_html', scope: [:utils])
@ -14,19 +14,19 @@
- if !user_signed_in?
.contact-champ
= label_tag :email do
= t('pro_mail', scope: [:supportadmin])
= t('.pro_mail')
%span.mandatory *
= text_field_tag :email, params[:email], required: true
.contact-champ
= label_tag :type do
= t('your_question', scope: [:support, :question])
= t('.your_question')
%span.mandatory *
= select_tag :type, options_for_select(@options, params[:type])
.contact-champ
= label_tag :phone do
= t('professional_phone_number', scope: [:supportadmin])
= t('.pro_phone_number')
= text_field_tag :phone
.contact-champ

View file

@ -1,16 +1,16 @@
- content_for(:title, 'Contact')
- content_for(:title, t('.contact'))
- content_for :footer do
= render partial: "root/footer"
#contact-form
.container
%h1.new-h1
= t('contact', scope: [:support])
= t('.contact')
= form_tag contact_path, method: :post, multipart: true, class: 'form' do |f|
.description
%p= t('intro_html', scope: [:support])
%p= t('.intro_html')
%br
%p.mandatory-explanation= t('asterisk_html', scope: [:utils])
@ -23,7 +23,7 @@
.contact-champ
= label_tag :type do
= t('your_question', scope: [:support, :question])
= t('.your_question')
%span.mandatory *
= hidden_field_tag :type, params[:type]
%dl
@ -37,9 +37,10 @@
%dd
.support.card.featured.hidden{ id: question_type }
.card-title
= t('our_answer', scope: [:support, :response])
= t('.our_answer')
.card-content
= t("#{question_type}_html", scope: [:support, :response], base_url: APPLICATION_BASE_URL, "link_#{question_type}": link)
-# i18n-tasks-use t("support.index.#{question_type}.answer_html")
= t('answer_html', scope: [:support, :index, question_type], base_url: APPLICATION_BASE_URL, "link_#{question_type}": link)
.contact-champ
= label_tag :dossier_id, t('file_number', scope: [:utils])
@ -61,9 +62,9 @@
= label_tag :piece_jointe do
= t('pj', scope: [:utils])
%p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } }
= t('notice_pj_product', scope: [:support, :response])
= t('.notice_pj_product')
%p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } }
= t('notice_pj_other', scope: [:support, :response])
= t('.notice_pj_other')
= file_field_tag :piece_jointe
= hidden_field_tag :tags, @tags&.join(',')

View file

@ -9,7 +9,7 @@
- etablissement = @dossier.etablissement
- if etablissement.diffusable_commercialement == false
%p= t('warning_for_private_info', etablissement: raison_sociale_or_name(etablissement), scope: 'views.shared.dossiers.identite_entreprise')
%p= t('warning_for_private_info', scope: 'views.shared.dossiers.identite_entreprise', etablissement: raison_sociale_or_name(etablissement))
- else
%p

View file

@ -14,10 +14,10 @@
.radios
%label
= f.radio_button :gender, Individual::GENDER_FEMALE, required: true
= t(Individual::GENDER_FEMALE, scope: 'activerecord.attributes.individual.gender_options')
= Individual.human_attribute_name('gender.female')
%label
= f.radio_button :gender, Individual::GENDER_MALE, required: true
= t('activerecord.attributes.individual.gender_options')[Individual::GENDER_MALE.to_sym] # GENDER_MALE contains a point letter
= Individual.human_attribute_name('gender.male')
.flex
.inline-champ

View file

@ -23,6 +23,22 @@
= f.email_field :email, value: nil, placeholder: 'Nouvelle adresse email', required: true
= f.submit "Changer mon adresse", class: 'button primary'
- if !instructeur_signed_in?
.card
.card-title= t('.transfer_title')
= t('.transfer_explication_html')
= form_tag transfer_all_dossiers_path, class: 'form' do
= email_field_tag :next_owner, nil, required: true
= submit_tag "Transférer tous mes dossiers", class: 'button primary', data: { confirm: t('.transfer_confirmation') }
- if @waiting_transfers.any?
.card.warning
.card-title= t('.waiting_transfers')
%ul
- @waiting_transfers.each do |email, nb_dossier|
%li= t('.one_waiting_transfer', email: email, count: nb_dossier)
- if current_administrateur.present?
.card
.card-title Jeton didentification de lAPI (token)

View file

@ -17,13 +17,11 @@
&nbsp;?
.email-suggestion-answer
= button_tag type: 'button', class: 'button small', onclick: "DS.acceptEmailSuggestion()" do
= t('simple_form.yes')
= t('utils.yes')
= button_tag type: 'button', class: 'button small', onclick: "DS.discardEmailSuggestionBox()" do
= t('simple_form.no')
= t('utils.no')
= 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
= f.submit t('views.shared.account.create'), class: "button large primary expand"

View file

@ -75,3 +75,6 @@ DS_ENV="staging"
# Désactivé l'OTP pour SuperAdmin
# SUPER_ADMIN_OTP_ENABLED = "disabled" # "enabled" par défaut
# API Particulier https://api.gouv.fr/les-api/api-particulier
# API_PARTICULIER_URL="https://particulier.api.gouv.fr/api"

View file

@ -29,8 +29,8 @@ data:
# External locale data (e.g. gems).
# This data is not considered unused and is never written to.
external:
## Example (replace %#= with %=):
# - "<%#= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml"
- "<%= %x[bundle info --path administrate].chomp %>/config/locales/*%{locale}.yml"
- "<%= %x[bundle info --path devise-i18n].chomp %>/rails/locales/*%{locale}.yml"
## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
# router: conservative_router
@ -96,9 +96,11 @@ search:
## Consider these keys used:
ignore_unused:
- 'activerecord.models.*'
- 'activerecord.attributes.*'
- 'activerecord.errors.*'
- 'errors.messages.blank'
- 'errors.messages.content_type_invalid'
- 'pluralize.*'
- 'views.pagination.*'
- 'time.formats.default'

View file

@ -1,34 +0,0 @@
# The following jobs were renamed, but instances using the old name
# were still scheduled to run on the job queue.
#
# To ensure the job queue can instantiate these jobs using the previous
# names, this file defines retro-compatibility aliases.
#
# Once all jobs running using the previous name will have run, this
# file can be safely deleted.
#
# (That probably means a few hours after deploying the rename in production
# - but let's keep these for a while to make external integrators's life easier.
# To keep some margin, let's say this file can be safely deleted in May 2021.)
Rails.application.reloader.to_prepare do
if !defined?(ApiEntreprise)
require 'excon'
module ApiEntreprise
Job = APIEntreprise::Job
AssociationJob = APIEntreprise::AssociationJob
AttestationFiscaleJob = APIEntreprise::AttestationFiscaleJob
AttestationSocialeJob = APIEntreprise::AttestationSocialeJob
BilansBdfJob = APIEntreprise::BilansBdfJob
EffectifsAnnuelsJob = APIEntreprise::EffectifsAnnuelsJob
EffectifsJob = APIEntreprise::EffectifsJob
EntrepriseJob = APIEntreprise::EntrepriseJob
ExercicesJob = APIEntreprise::ExercicesJob
end
module Cron
FixMissingAntivirusAnalysis = FixMissingAntivirusAnalysisJob
end
end
end

View file

@ -25,8 +25,8 @@ end
# A list of features to be deployed on first push
features = [
:administrateur_routage,
:administrateur_web_hook,
:api_particulier,
:dossier_pdf_vide,
:expert_not_allowed_to_invite,
:hide_instructeur_email,

View file

@ -2,6 +2,7 @@
# API URLs
API_ENTREPRISE_URL = ENV.fetch("API_ENTREPRISE_URL", "https://entreprise.api.gouv.fr/v2")
API_EDUCATION_URL = ENV.fetch("API_EDUCATION_URL", "https://data.education.gouv.fr/api/records/1.0")
API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")

View file

@ -0,0 +1,5 @@
en:
errors:
messages:
content_type_invalid: "is not of an accepted type"
file_size_out_of_range: "is too big. The file must be at most %{file_size_limit}."

View file

@ -1,5 +1,5 @@
fr:
errors:
messages:
content_type_invalid: nest pas dun type accepté
file_size_out_of_range: "est trop lourde, elle doit faire au plus 200 Mo."
content_type_invalid: "nest pas dun type accepté"
file_size_out_of_range: "est trop lourd(e). Le fichier doit faire au plus %{file_size_limit}."

View file

@ -0,0 +1,30 @@
fr:
api_particulier:
providers:
cnaf:
libelle: Caisse dallocations familiales (CAF)
scopes:
personne: &personne
noms_prenoms: noms et prénoms
date_de_naissance: date de naissance
sexe: genre
allocataires:
libelle: allocataires
<<: *personne
enfants:
libelle: enfants
<<: *personne
adresse:
libelle: adresse
identite: identité
complement_d_identite: complément didentité
complement_d_identite_geo: complément didentité géographique
numero_et_rue: numéro et rue
lieu_dit: lieu-dit
code_postal_et_ville: code postal et ville
pays: pays
quotient_familial:
libelle: quotient familial
quotient_familial: quotient familial
mois: mois
annee: année

View file

@ -32,6 +32,8 @@
en:
help: 'Help'
utils:
'yes': Yes
'no': No
deconnexion: "Log out"
pj: "Attachments"
asterisk_html: Fields marked by an asterisk ( <span class = mandatory>*</span> ) are mandatory.
@ -64,7 +66,6 @@ en:
start_procedure: Start the procedure
existing_dossiers: You already have files for this procedure
show_dossiers: View my current files
start_new_dossier: Start a new file
already_draft: "You already started to fill a file"
already_draft_detail_html: "You started to fill a file for the \"%{procedure}\" procedure <strong>%{time_ago} ago</strong>"
already_not_draft: "You already submitted a file"
@ -185,7 +186,6 @@ en:
status: "Status"
updated: "Updated"
actions: "Actions"
accessibility_question: "What do you think about the accessibility of this service?"
dossier_action:
edit_dossier: "Edit the file"
start_other_dossier: "Start an other file"
@ -203,7 +203,7 @@ en:
connection: Sign in
are_you_new: First time on %{app_name}?
find_procedure: Find your procedure
password:
passwords:
reset_link_sent:
got_it: Got it!
open_your_mailbox: Now open your mailbox.
@ -213,9 +213,9 @@ en:
check_spams: Check your junk or spam email.
check_account: Have you created a %{application_name} account with the adress %{email}? You will not receive any message if no account is linked to your email adress.
check_france_connect_html: Have you once logged in with France Connect? If yes, <a href=\"%{href}\">try again with France Connect</a>.
shared:
email_can_take_a_while_html: <strong>Please note</strong> that this message can take up to 15 minutes to arrive.
contact_us_if_any_trouble_html: You can contact us <a href=\"%{href}\">through this form</a> if a problem still exists.
shared:
email_can_take_a_while_html: <strong>Please note</strong> that this message can take up to 15 minutes to arrive.
contact_us_if_any_trouble_html: You can contact us <a href=\"%{href}\">through this form</a> if a problem still exists.
modal:
publish:
title:
@ -352,13 +352,20 @@ en:
users:
dossiers:
test_procedure: "This file is submitted on a test procedure. Any modification of the procedure by the administrator (addition of a field, publication of the procedure, etc.) will result in the removal of the file."
message_send: "Your message has been sent to the instructor in charge of your file."
no_access: "You do not have access to this file"
no_longer_editable: "Your file can no longer be edited"
undergoingreview: "Your file is undergoing review. It is no longer possible to delete your file. To cancel the undergoingreview contact the adminitration via the mailbox."
deleted_dossier: "Your file has been successfully deleted"
archived_dossier: "Your file will be archived for an additional month"
draft_saved: "Your draft has been saved."
no_establishment: "There is no establishment tied to this file"
identity_saved: "Identity data is registred"
no_longer_available: "The certificate is no longer available on this file."
create_commentaire:
message_send: "Your message has been sent to the instructor in charge of your file."
ask_deletion:
undergoingreview: "Your file is undergoing review. It is no longer possible to delete your file. To cancel the undergoingreview contact the adminitration via the mailbox."
deleted_dossier: "Your file has been successfully deleted"
extend_conservation:
archived_dossier: "Your file will be archived for an additional month"
update_brouillon:
draft_saved: "Your draft has been saved."
etablissement:
no_establishment: "There is no establishment tied to this file"
update_identite:
identity_saved: "Identity data is registred"
attestation:
no_longer_available: "The certificate is no longer available on this file."

View file

@ -22,6 +22,8 @@
fr:
help: 'Aide'
utils:
'yes': Oui
'no': Non
deconnexion: "Déconnexion"
pj: "Pièces jointes"
asterisk_html: Les champs suivis dun astérisque ( <span class = mandatory> * </span> ) sont obligatoires.
@ -97,6 +99,9 @@ fr:
first: Premier
truncate: '&hellip;'
shared:
actions:
save: Enregistrer
edit: Modifier
greetings:
hello: Bonjour,
best_regards: Bonne journée,
@ -177,7 +182,6 @@ fr:
status: "Statut"
updated: "Mis à jour"
actions: "Actions"
accessibility_question: "Que pensez-vous de la facilité dutilisation de ce service ?"
dossier_action:
edit_dossier: "Modifier le dossier"
start_other_dossier: "Commencer un autre dossier"
@ -356,13 +360,51 @@ fr:
users:
dossiers:
test_procedure: "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par ladministrateur (ajout d'un champ, publication de la démarche...) entraînera sa suppression."
message_send: "Votre message a bien été envoyé à linstructeur en charge de votre dossier."
no_access: "Vous navez pas accès à ce dossier"
no_longer_editable: "Votre dossier ne peut plus être modifié"
undergoingreview: "Linstruction de votre dossier a commencé, il nest plus possible de supprimer votre dossier. Si vous souhaitez annuler linstruction contactez votre administration par la messagerie de votre dossier."
deleted_dossier: "Votre dossier a bien été supprimé."
archived_dossier: "Votre dossier sera conservé un mois supplémentaire"
draft_saved: "Votre brouillon a bien été sauvegardé."
no_establishment: "Aucun établissement nest associé à ce dossier"
identity_saved: "Identité enregistrée"
no_longer_available: "Lattestation n'est plus disponible sur ce dossier."
create_commentaire:
message_send: "Votre message a bien été envoyé à linstructeur en charge de votre dossier."
ask_deletion:
undergoingreview: "Linstruction de votre dossier a commencé, il nest plus possible de supprimer votre dossier. Si vous souhaitez annuler linstruction contactez votre administration par la messagerie de votre dossier."
deleted_dossier: "Votre dossier a bien été supprimé."
extend_conservation:
archived_dossier: "Votre dossier sera conservé un mois supplémentaire"
update_brouillon:
draft_saved: "Votre brouillon a bien été sauvegardé."
etablissement:
no_establishment: "Aucun établissement nest associé à ce dossier"
update_identite:
identity_saved: "Identité enregistrée"
attestation:
no_longer_available: "Lattestation n'est plus disponible sur ce dossier."
new_administrateur:
procedures:
show:
ready: "Validé"
needs_configuration: "À configurer"
configure_api_particulier_token: "Configurer le jeton API particulier"
jeton_particulier:
show:
configure_token: "Configurer le jeton API Particulier"
api_particulier_description_html: "%{app_name} utilise <a href=\"https://api.gouv.fr/les-api/api-particulier\">API Particulier</a> qui permet de récupérer les données familiales (CAF).<br />Renseignez ici le <a href=\"https://api.gouv.fr/les-api/api-particulier/demande-acces\">jeton API Particulier</a> propre à votre démarche."
token_description: "Il doit contenir au minimum 15 caractères."
update:
token_ok: "Le jeton a bien été mis à jour"
no_scopes_token: "Mise à jour impossible : le jeton n'a pas acces aux données.<br /><br />Vérifier le auprès de <a href='https://datapass.api.gouv.fr/'>https://datapass.api.gouv.fr/</a>"
not_found_token: "Mise à jour impossible : le jeton n'a pas été trouvé ou n'est pas actif<br /><br />Vérifier le auprès de <a href='https://datapass.api.gouv.fr/'>https://datapass.api.gouv.fr/</a>"
network_error: "Mise à jour impossible : une erreur réseau est survenue"
api_particulier:
already_configured: "Déjà rempli"
needs_configuration: "À remplir"
sources_particulier:
show:
title: "Définir les sources de données"
data_sources: "Sources de données"
explication_html: "<p>API Particulier facilite laccès des administrations aux données familiales (CAF) pour simplifier les démarches administratives mises en œuvre par les collectivités et les administrations.<br> Cela permet aux administrations daccéder à des informations certifiées à la source et ainsi : </p> <ul> <li>de saffranchir des pièces justificatives lors des démarches en ligne,</li> <li>de réduire le nombre derreurs de saisie,</li> <li>décarter le risque de fraude documentaire.</li> </ul> <p> <strong>Important&nbsp;:</strong> les disposition de l'article <a href='https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000031366350&amp;idArticle=LEGIARTI000031367412&amp;dateTexte=&amp;categorieLien=cid'>L144-8</a> nautorisent que léchange des informations strictement nécessaires pour traiter une démarche.<br /><br />En conséquence, ne sélectionnez ici que les données auxquelles vous aurez accès dun point de vue légal.</p>"
update:
sources_ok: 'Mise à jour effectuée'
procedures:
show:
ready: "Validé"
needs_configuration: "À configurer"
configure_api_particulier_token: "Configurer le jeton API particulier"

View file

@ -6,6 +6,6 @@ en:
nom: Last name
prenom: First name
birthdate: Date de naissance
gender_options:
"Mme": "Ms"
"M.": "Mr"
individual/gender:
female: Ms
male: Mr

View file

@ -6,6 +6,6 @@ fr:
nom: Nom
prenom: Prénom
birthdate: Date de naissance
gender_options:
"Mme": "Madame"
"M.": "Monsieur"
individual/gender:
female: Madame
male: Monsieur

View file

@ -16,3 +16,11 @@ fr:
aasm_state/hidden: Suprimée
declarative_with_state/en_instruction: En instruction
declarative_with_state/accepte: Accepté
api_particulier_token: Jeton API Particulier
errors:
models:
procedure:
attributes:
api_particulier_token:
invalid: 'na pas le bon format'

View file

@ -1,31 +0,0 @@
en:
simple_form:
"yes": 'Yes'
"no": 'No'
required:
text: 'required'
mark: '*'
# You can uncomment the line below if you need to overwrite the whole required html.
# When using html, text and mark wont be used.
# html: '<abbr title="required">*</abbr>'
error_notification:
default_message: "Please review the problems below:"
# Examples
# labels:
# defaults:
# password: 'Password'
# user:
# new:
# email: 'E-mail to sign in.'
# edit:
# email: 'E-mail.'
# hints:
# defaults:
# username: 'User name to sign in.'
# password: 'No special characters, please.'
# include_blanks:
# defaults:
# age: 'Rather not say'
# prompts:
# defaults:
# age: 'Select your age'

View file

@ -1,31 +0,0 @@
fr:
simple_form:
"yes": 'Oui'
"no": 'Non'
required:
text: 'obligatoire'
mark: '*'
# You can uncomment the line below if you need to overwrite the whole required html.
# When using html, text and mark won't be used.
# html: '<abbr title="required">*</abbr>'
error_notification:
default_message: "Erreur, veuillez vérifier vos réponses:"
# Examples
# labels:
# defaults:
# password: 'Password'
# user:
# new:
# email: 'E-mail to sign in.'
# edit:
# email: 'E-mail.'
# hints:
# defaults:
# username: 'User name to sign in.'
# password: 'No special characters, please.'
# include_blanks:
# defaults:
# age: 'Rather not say'
# prompts:
# defaults:
# age: 'Select your age'

View file

@ -1,4 +1,4 @@
fr:
en:
dossier_mailer:
notify_new_answer:
subject: New message on your file nº %{dossier_id} « %{libelle_demarche} »

View file

@ -3,7 +3,7 @@ fr:
notify_new_answer:
subject: Nouveau message pour votre dossier nº %{dossier_id} « %{libelle_demarche} »
body_html: |
Vous avez reçu un <b>nouveau message</b> de la part du service en charge de votre dossier.
Vous avez reçu un <b>nouveau message</b> de la part du service en charge de votre dossier sur la démarche « %{libelle_demarche} ».
link: |
Pour consulter le message et y répondre, cliquez sur le bouton ci-dessous :
body_draft_html: |

View file

@ -3,7 +3,7 @@ fr:
notify_revert_to_instruction:
subject: Votre dossier nº %{dossier_id} sur la démarche « %{libelle_demarche} » est en train dêtre réexaminé
body_html: |
Votre dossier va être réexaminé, la précédente décision sur ce dossier est caduque.
Votre dossier %{dossier_id} va être réexaminé, la précédente décision sur ce dossier est caduque.
Vous pouvez retrouver le dossier que vous avez créé pour la démarche <b>« %{libelle_demarche} »</b> à l'adresse suivante :
contact: |
Pour obtenir le détail de cette modification de la décision, vous pouvez contacter par email :

View file

@ -4,7 +4,8 @@ fr:
wrong_address:
one: "%{value} nest pas une adresse email valide"
other: "%{value} ne sont pas des adresses emails valides"
experts_assignment:
create:
experts_assignment:
one: "L'expert %{value} a été affecté à la démarche n° %{procedure}"
other: "Les experts %{value} ont été affectés à la démarche n° %{procedure}"
groupe_instructeurs:
@ -12,10 +13,6 @@ fr:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
show:
assigned_instructeur:
one: "%{count} instructeur est affecté"
other: "%{count} instructeurs sont affectés"
add_instructeur:
wrong_address:
one: "%{value} nest pas une adresse email valide"
@ -27,3 +24,18 @@ fr:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
instructeurs:
assigned_instructeur:
one: "%{count} instructeur est affecté"
other: "%{count} instructeurs sont affectés"
edit:
existing_groupe:
one: "%{count} groupe existe"
other: "%{count} groupes existent"
routing:
notice_html: |
Le routage est une fonctionnalité pour les démarches nécessitant le partage de linstruction entre différents groupes en fonction dun critère précis (territoire, thématique ou autre).
<br><br>
Cette fonctionnalité permet dacheminer les dossier vers chaque groupe, et de ne plus avoir besoin de filtrer ses dossiers parmi une grande quantité de demandes. Elle est donc particulièrement adaptée pour les démarches nationales instruites localement.
<br><br>
Les instructeurs ne voient que les dossiers les concernant, et nont donc pas accès aux données extérieures à leur périmètre.

View file

@ -11,8 +11,8 @@ fr:
update_description: La description du champ « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_type_champ: Le type du champ « %{label} » a été modifié. Il est maintenant de type « %{to} »
update_mandatory:
enable: Le champ « %{label} » est maintenant obligatoire
disable: Le champ « %{label} » nest plus obligatoire
enabled: Le champ « %{label} » est maintenant obligatoire
disabled: Le champ « %{label} » nest plus obligatoire
update_piece_justificative_template: Le modèle de pièce justificative du champ « %{label} » a été modifié
update_drop_down_options: Les options de sélection du champ « %{label} » ont été modifiées
update_carte_layers: Les référentiels cartographiques du champ « %{label} » ont été modifiés
@ -25,8 +25,8 @@ fr:
update_description_private: La description de lannotation privée « %{label} » a été modifiée. La nouvelle description est « %{to} »
update_type_champ_private: Le type de lannotation privée « %{label} » a été modifié. Elle est maintenant de type « %{to} »
update_mandatory_private:
enable: Lannotation privée « %{label} » est maintenant obligatoire
disable: Lannotation privée « %{label} » nest plus obligatoire
enabled: Lannotation privée « %{label} » est maintenant obligatoire
disabled: Lannotation privée « %{label} » nest plus obligatoire
update_piece_justificative_template_private: Le modèle de pièce justificative de lannotation privée « %{label} » a été modifié
update_drop_down_options_private: Les options de sélection de lannotation privée « %{label} » ont été modifiées
update_carte_layers_private: Les référentiels cartographiques de lannotation privée « %{label} » ont été modifiés

View file

@ -0,0 +1,56 @@
en:
support:
index:
contact: Contact
intro_html: Contact us via this form and we will answer you as quickly as possible.<br>Make sure you provide all the required information so we can help you in the best way.
your_question: Your question
our_answer: 👉 Our answer
notice_pj_product: A screenshot can help us identify the element to improve.
notice_pj_other: A screenshot can help us identify the issue.
procedure_info:
question: I've encountered a problem while completing my application
answer_html: "<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?
<p>If you have questions about the information requested, contact the service in charge of the procedure.</p>
<p><a href=%{link_procedure_info}>Find more information</a></p>"
instruction_info:
question: I have a question about the instruction of my application
answer_html: "<p>If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system.</p>
<p><a href=%{link_instruction_info}>Find more information</a></p>
<br>
<p>If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.</p>"
product:
question: I have an idea to improve the website
answer_html: "<p>Got an idea? Please check our <strong>enhancement dashboard</strong></p>
<p><ul><li>Vote for your priority improvements</li>
<li>Share your own ideas</li></ul></p>
<p><strong><a href=%{link_product}>➡ Access the enhancement dashboard</a></strong></p>"
lost_user:
question: I am having trouble finding the procedure I am looking for
answer_html: "<p>We invite you to contact the administration in charge of the procedure so they can provide you the link.
It should look like this: %{base_url}/commencer/NOM_DE_LA_DEMARCHE.</p>
<br>
<p>You can find here the most popular procedures (licence, detr, etc.) :</p>
<p><a href=%{link_lost_user}>%{link_lost_user}</a></p>"
other:
question: Other topic
admin:
your_question: Your question
admin_intro_html: "<p>As an administration, you can contact us through this form. We'll answer you as quickly as possibly by e-mail or phone.</p>
<br>
<p><strong>Caution, this form is dedicated to public bodies only.</strong>
It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us <a href=%{contact_path}>here</a>.</p>"
contact_team: Contact our team
pro_phone_number: Professional phone number (direct line)
pro_mail: Professional email address
admin question:
question: I have a question about %{app_name}
admin demande rdv:
question: I request an appointment for an online presentation of %{app_name}
admin soucis:
question: I am facing a technical issue on %{app_name}
admin suggestion produit:
question: I have a suggestion for an evolution
admin demande compte:
question: I want to open an admin account with an Orange, Wanadoo, etc. email
admin autre:
question: Other topic

View file

@ -0,0 +1,56 @@
fr:
support:
index:
contact: Contact
intro_html: Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.<br>Pensez bien à nous donner le plus dinformations possible pour que nous puissions vous aider au mieux.
your_question: Votre question
our_answer: 👉 Notre réponse
notice_pj_product: Une capture décran peut nous aider à identifier plus facilement lendroit à améliorer.
notice_pj_other: Une capture décran peut nous aider à identifier plus facilement le problème.
procedure_info:
question: Jai un problème lors du remplissage de mon dossier
answer_html: "<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?
<p>Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.</p>
<p><a href=%{link_procedure_info}>En savoir plus</a></p>"
instruction_info:
question: Jai une question sur linstruction de mon dossier
answer_html: "<p>Si vous avez des questions sur linstruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie.</p>
<p><a href=%{link_instruction_info}>En savoir plus</a></p>
<br>
<p>Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur linstruction de votre dossier.</p>"
product:
question: Jai une idée damélioration pour votre site
answer_html: "<p>Une idée ? Pensez à consulter notre <strong>tableau de bord des améliorations</strong></p>
<p><ul><li>Votez pour vos améliorations prioritaires;</li>
<li>Proposez votre propre idée.</li></ul></p>
<p><strong><a href=%{link_product}>➡ Accéder au tableau des améliorations</a></strong></p>"
lost_user:
question: Je ne trouve pas la démarche que je veux faire
answer_html: "<p>Nous vous invitons à contacter ladministration en charge de votre démarche pour quelle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : %{base_url}/commencer/NOM_DE_LA_DEMARCHE.</p>
<br>
<p>Vous pouvez aussi consulter ici la liste de nos démarches les plus frequentes (permis, detr etc) :</p>
<p><a href=%{link_lost_user}>%{link_lost_user}</a></p>"
other:
question: Autre sujet
admin:
your_question: Votre question
admin_intro_html: "<p>En tant quadministration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.</p>
<br>
<p><strong>Attention, ce formulaire est réservé uniquement aux organismes publics.</strong>
Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues dutilité publique). Si c'est votre cas, rendez-vous sur notre
<a href=%{contact_path}>formulaire de contact public</a>.</p>"
contact_team: Contactez notre équipe
pro_phone_number: Numéro de téléphone professionnel (ligne directe)
pro_mail: Adresse e-mail professionnelle
admin question:
question: Jai une question sur %{app_name}
admin demande rdv:
question: Demande de RDV pour une présentation à distance de %{app_name}
admin soucis:
question: Jai un problème technique avec %{app_name}
admin suggestion produit:
question: Jai une proposition dévolution
admin demande compte:
question: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc.
admin autre:
question: Autre sujet

View file

@ -1,46 +0,0 @@
en:
support:
contact: Contact
intro_html: Contact us via this form and we will answer you as quickly as possible.<br>Make sure you provide all the required information so we can help you in the best way.
question:
your_question: Your question
choose_question: Choose your question
procedure_info: I've encountered a problem while completing my application
instruction_info: I have a question about the instruction of my application
product: I have an idea to improve the website
lost_user: I am having trouble finding the procedure I am looking for
other: Other topic
response:
our_answer: 👉 Our answer
procedure_info_html: "<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?
<p>If you have questions about the information requested, contact the service in charge of the procedure.</p>
<p><a href=%{link_procedure_info}>Find more information</a></p>"
instruction_info_html: "<p>If you have questions about the instruction of your application (response delay for example), contact directly the instructor via our mail system.</p>
<p><a href=%{link_instruction_info}>Find more information</a></p>
<br>
<p>If you are facing technical issues on the website, use the form below. We will not be able to inform you about the instruction of your application.</p>"
product_html: "<p>Got an idea? Please check our <strong>enhancement dashboard</strong></p>
<p><ul><li>Vote for your priority improvements</li>
<li>Share your own ideas</li></ul></p>
<p><strong><a href=%{link_product}>➡ Access the enhancement dashboard</a></strong></p>"
lost_user_html: "<p>We invite you to contact the administration in charge of the procedure so they can provide you the link.
It should look like this: %{base_url}/commencer/NOM_DE_LA_DEMARCHE.</p>
<br>
<p>You can find here the most popular procedures (licence, detr, etc.) :</p>
<p><a href=%{link_lost_user}>%{link_lost_user}</a></p>"
notice_pj_product: A screenshot can help us identify the element to improve.
notice_pj_other: A screenshot can help us identify the issue.
supportadmin:
admin_intro_html: "<p>As an administration, you can contact us through this form. We'll answer you as quickly as possibly by e-mail or phone.</p>
<br>
<p><strong>Caution, this form is dedicated to public bodies only.</strong>
It does not concern individuals, companies nor associations (except those recognised of public utility). If you belong to one of these categories, contact us <a href=%{contact_path}>here</a>.</p>"
contact_team: Contact our team
pro_phone_number: Professional phone number (direct line)
pro_mail: Professional email address
admin demande rdv: I request an appointment for an online presentation of %{app_name}
admin question: I have a question about %{app_name}
admin soucis: I am facing a technical issue on %{app_name}
admin suggestion produit: I have a suggestion for an evolution
admin demande compte: I want to open an admin account with an Orange, Wanadoo, etc. email
admin autre: Other topic

View file

@ -1,46 +0,0 @@
fr:
support:
contact: Contact
intro_html: Contactez-nous via ce formulaire et nous vous répondrons dans les plus brefs délais.<br>Pensez bien à nous donner le plus dinformations possible pour que nous puissions vous aider au mieux.
question:
your_question: Votre question
choose_question: Choisir une question
procedure_info: Jai un problème lors du remplissage de mon dossier
instruction_info: Jai une question sur linstruction de mon dossier
product: Jai une idée damélioration pour votre site
lost_user: Je ne trouve pas la démarche que je veux faire
other: Autre sujet
response:
our_answer: 👉 Notre réponse
procedure_info_html: "<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?
<p>Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.</p>
<p><a href=%{link_procedure_info}>En savoir plus</a></p>"
instruction_info_html: "<p>Si vous avez des questions sur linstruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie.</p>
<p><a href=%{link_instruction_info}>En savoir plus</a></p>
<br>
<p>Si vous souhaitez poser une question pour un problème technique sur le site, utilisez le formulaire ci-dessous. Nous ne pourrons pas vous renseigner sur linstruction de votre dossier.</p>"
product_html: "<p>Une idée ? Pensez à consulter notre <strong>tableau de bord des améliorations</strong></p>
<p><ul><li>Votez pour vos améliorations prioritaires;</li>
<li>Proposez votre propre idée.</li></ul></p>
<p><strong><a href=%{link_product}>➡ Accéder au tableau des améliorations</a></strong></p>"
lost_user_html: "<p>Nous vous invitons à contacter ladministration en charge de votre démarche pour quelle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : %{base_url}/commencer/NOM_DE_LA_DEMARCHE.</p>
<br>
<p>Vous pouvez aussi consulter ici la liste de nos démarches les plus frequentes (permis, detr etc) :</p>
<p><a href=%{link_lost_user}>%{link_lost_user}</a></p>"
notice_pj_product: Une capture décran peut nous aider à identifier plus facilement lendroit à améliorer.
notice_pj_other: Une capture décran peut nous aider à identifier plus facilement le problème.
supportadmin:
admin_intro_html: "<p>En tant quadministration, vous pouvez nous contactez via ce formulaire. Nous vous répondrons dans les plus brefs délais, par email ou par téléphone.</p>
<br>
<p><strong>Attention, ce formulaire est réservé uniquement aux organismes publics.</strong>
Il ne concerne ni les particuliers, ni les entreprises, ni les associations (sauf celles reconnues dutilité publique). Si c'est votre cas, rendez-vous sur notre
<a href=%{contact_path}>formulaire de contact public</a>.</p>"
contact_team: Contactez notre équipe
pro_phone_number: Numéro de téléphone professionnel (ligne directe)
pro_mail: Adresse e-mail professionnelle
admin demande rdv: Demande de RDV pour une présentation à distance de %{app_name}
admin question: Jai une question sur %{app_name}
admin soucis: Jai un problème technique avec %{app_name}
admin suggestion produit: Jai une proposition dévolution
admin demande compte: Je souhaite ouvrir un compte administrateur avec un email Orange, Wanadoo, etc.
admin autre: Autre sujet

View file

@ -0,0 +1,16 @@
fr:
users:
profil:
show:
transfer_title: Transferer tous vos dossiers
transfer_explication_html: "<p>Cette fonctionnalité vous permet de changer le propriétaire de tous vos dossiers. C'est généralement utile lors d'un changement de poste ou si vous souhaitez fusionner plusieurs comptes.</p>
<p>Adresse email du destinataire de tous vos dossiers</p>"
waiting_transfers: "Transfert en attente :"
one_waiting_transfer:
one: "Le nouveau propriétaire %{email} doit confirmer le transfert d'un dossier en suivant les instructions reçues dans son mail."
other: "Le nouveau propriétaire %{email} doit confirmer le transfert de vos %{count} dossiers en suivant les instructions reçues dans son mail."
transfer_confirmation: "Confirmez-vous le transfert ?"
transfer_all_dossiers:
new_transfer:
one: "Le transfert d'un dossier à %{email} est en cours"
other: "Le transfert de %{count} dossiers à %{email} est en cours"

View file

@ -276,6 +276,7 @@ Rails.application.routes.draw do
# allow refresh 'renew api token' page
get 'renew-api-token' => redirect('/profil')
patch 'update_email' => 'profil#update_email'
post 'transfer_all_dossiers' => 'profil#transfer_all_dossiers'
end
#
@ -397,6 +398,13 @@ Rails.application.routes.draw do
put :experts_require_administrateur_invitation
end
get :api_particulier, controller: 'jeton_particulier'
resource 'api_particulier', only: [] do
resource 'jeton', only: [:show, :update], controller: 'jeton_particulier'
resource 'sources', only: [:show, :update], controller: 'sources_particulier'
end
put 'clone'
put 'archive'
get 'publication' => 'procedures#publication', as: :publication
@ -415,6 +423,7 @@ Rails.application.routes.draw do
collection do
patch 'update_routing_criteria_name'
patch 'update_routing_enabled'
post 'import'
end
end

View file

@ -0,0 +1,5 @@
class AddAPIParticulierScopesToProcedure < ActiveRecord::Migration[6.1]
def change
add_column :procedures, :api_particulier_scopes, :text, array: true, default: []
end
end

View file

@ -0,0 +1,6 @@
class AddAPIParticulierSourcesToProcedure < ActiveRecord::Migration[6.0]
def change
add_column :procedures, :api_particulier_sources, :jsonb, :default => {}
add_index :procedures, :api_particulier_sources, using: :gin
end
end

View file

@ -0,0 +1,5 @@
class AddRoutingEnabledToProcedures < ActiveRecord::Migration[6.1]
def change
add_column :procedures, :routing_enabled, :boolean
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_08_27_161956) do
ActiveRecord::Schema.define(version: 2021_09_15_170019) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -617,8 +617,12 @@ ActiveRecord::Schema.define(version: 2021_08_27_161956) do
t.bigint "draft_revision_id"
t.bigint "published_revision_id"
t.boolean "allow_expert_review", default: true, null: false
t.string "encrypted_api_particulier_token"
t.boolean "experts_require_administrateur_invitation", default: false
t.string "encrypted_api_particulier_token"
t.text "api_particulier_scopes", default: [], array: true
t.jsonb "api_particulier_sources", default: {}
t.index ["api_particulier_sources"], name: "index_procedures_on_api_particulier_sources", using: :gin
t.boolean "routing_enabled"
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id"
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"

View file

@ -2,6 +2,9 @@ task :lint do
sh "bundle exec rubocop --parallel"
sh "bundle exec haml-lint app/views/"
sh "bundle exec scss-lint app/assets/stylesheets/"
sh "bundle exec i18n-tasks missing --locales fr"
sh "bundle exec i18n-tasks unused --locale en" # TODO: check for all locales
sh "bundle exec i18n-tasks check-consistent-interpolations"
sh "bundle exec brakeman --no-pager"
sh "yarn lint:js"
end

View file

@ -17,12 +17,27 @@ describe DevisePopulatedResource, type: :controller do
end
context 'when initiating a password reset' do
subject { get :edit, params: { reset_password_token: @token } }
subject { get :edit, params: { reset_password_token: token } }
it 'returns the fully populated resource' do
subject
expect(controller.populated_resource.id).to eq(user.id)
expect(controller.populated_resource.email).to eq(user.email)
context 'with a valid token' do
let(:token) { @token }
it 'returns the fully populated resource' do
subject
expect(controller.populated_resource.id).to eq(user.id)
expect(controller.populated_resource.email).to eq(user.email)
end
end
context 'with an expired token' do
let(:token) { 'invalid-token' }
it 'returns a new blank resource' do
subject
expect(controller.populated_resource).to be_present
expect(controller.populated_resource.new_record?).to be(true)
expect(controller.populated_resource.email).to be_blank
end
end
end

View file

@ -2,7 +2,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
render_views
let(:admin) { create(:administrateur) }
let(:procedure) { create(:procedure, :published, :for_individual, administrateurs: [admin]) }
let(:procedure) { create(:procedure, :published, :for_individual, administrateurs: [admin], routing_enabled: true) }
let!(:gi_1_1) { procedure.defaut_groupe_instructeur }
let(:procedure2) { create(:procedure, :published) }
@ -222,7 +222,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(response.status).to eq(200) }
it { expect(subject.request.flash[:alert]).to be_nil }
it { expect(subject.request.flash[:notice]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
context 'when there is at least one bad email' do
@ -230,13 +230,13 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(response.status).to eq(200) }
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject.request.flash[:notice]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
context 'when the admin wants to assign an instructor who is already assigned on this procedure' do
let(:emails) { ['instructeur_1@ministere_a.gouv.fr'].to_json }
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
end
@ -344,7 +344,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(subject.request.flash[:notice]).to be_present }
it { expect(subject.request.flash[:alert]).to be_nil }
it { expect(response.status).to eq(302) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, gi_1_1) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
context 'when the instructor is not assigned to the procedure' do
@ -352,7 +352,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
it { expect(subject.request.flash[:alert]).to be_present }
it { expect(subject.request.flash[:notice]).to be_nil }
it { expect(response.status).to eq(302) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, gi_1_1) }
it { expect(subject).to redirect_to admin_procedure_groupe_instructeurs_path(procedure) }
end
end

View file

@ -0,0 +1,87 @@
describe NewAdministrateur::JetonParticulierController, type: :controller do
let(:admin) { create(:administrateur) }
let(:procedure) { create(:procedure, administrateur: admin) }
before do
stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
sign_in(admin.user)
end
describe "GET #api_particulier" do
render_views
subject { get :api_particulier, params: { procedure_id: procedure.id } }
it do
is_expected.to have_http_status(:success)
expect(subject.body).to have_content('Jeton API particulier')
end
end
describe "GET #show" do
subject { get :show, params: { procedure_id: procedure.id } }
it { is_expected.to have_http_status(:success) }
end
describe "PATCH #update" do
let(:params) { { procedure_id: procedure.id, procedure: { api_particulier_token: token } } }
subject { patch :update, params: params }
context "when jeton has a valid shape" do
let(:token) { "d7e9c9f4c3ca00caadde31f50fd4521a" }
before do
VCR.use_cassette(cassette) do
subject
end
end
context "and the api response is a success" do
let(:cassette) { "api_particulier/success/introspect" }
let(:procedure) { create(:procedure, administrateur: admin, api_particulier_sources: { cnaf: { allocataires: ['noms_prenoms'] } }) }
it 'saves the jeton' do
expect(flash.alert).to be_nil
expect(flash.notice).to eq("Le jeton a bien été mis à jour")
expect(procedure.reload.api_particulier_token).to eql(token)
expect(procedure.reload.api_particulier_scopes).to contain_exactly("dgfip_avis_imposition", "dgfip_adresse", "cnaf_allocataires", "cnaf_enfants", "cnaf_adresse", "cnaf_quotient_familial", "mesri_statut_etudiant")
expect(procedure.reload.api_particulier_sources).to be_empty
end
end
context "and the api response is a success but with an empty scopes" do
let(:cassette) { "api_particulier/success/introspect_empty_scopes" }
it 'rejects the jeton' do
expect(flash.alert).to include("le jeton n'a pas acces aux données")
expect(flash.notice).to be_nil
expect(procedure.reload.api_particulier_token).not_to eql(token)
end
end
context "and the api response is not unauthorized" do
let(:cassette) { "api_particulier/unauthorized/introspect" }
it 'rejects the jeton' do
expect(flash.alert).to include("Mise à jour impossible : le jeton n'a pas été trouvé ou n'est pas actif")
expect(flash.notice).to be_nil
expect(procedure.reload.api_particulier_token).not_to eql(token)
end
end
end
context "when jeton is invalid and no network call is made" do
let(:token) { "jet0n 1nvalide" }
before { subject }
it 'rejects the jeton' do
expect(flash.alert.first).to include("pas le bon format")
expect(flash.notice).to be_nil
expect(procedure.reload.api_particulier_token).not_to eql(token)
end
end
end
end

View file

@ -0,0 +1,60 @@
describe NewAdministrateur::SourcesParticulierController, type: :controller do
let(:admin) { create(:administrateur) }
before { sign_in(admin.user) }
describe "#show" do
let(:procedure) { create(:procedure, administrateur: admin, api_particulier_scopes: ['cnaf_enfants'], api_particulier_sources: { cnaf: { enfants: ['noms_prenoms'] } }) }
render_views
subject { get :show, params: { procedure_id: procedure.id } }
it 'renders the sources form' do
expect(subject.body).to include(I18n.t('api_particulier.providers.cnaf.scopes.enfants.date_de_naissance'))
expect(subject.body).to have_selector("input#api_particulier_sources_cnaf_enfants_[value=noms_prenoms][checked=checked]")
expect(subject.body).to have_selector("input#api_particulier_sources_cnaf_enfants_[value=date_de_naissance]")
expect(subject.body).not_to have_selector("input#api_particulier_sources_cnaf_enfants_[value=date_de_naissance][checked=checked]")
end
end
describe "#update" do
let(:procedure) { create(:procedure, administrateur: admin, api_particulier_scopes: ['cnaf_enfants'], api_particulier_sources: {}) }
let(:params) { { procedure_id: procedure.id }.merge(requested_sources) }
before do
patch :update, params: params
procedure.reload
end
context 'when no source is requested' do
let(:requested_sources) { {} }
it { expect(procedure.api_particulier_sources).to be_empty }
end
context 'when a forbidden source is requested' do
let(:requested_sources) do
{
api_particulier_sources: { cnaf: { enfants: ['forbidden'] } }
}
end
it { expect(procedure.api_particulier_sources).to be_empty }
end
context 'when an authorized source is requested' do
let(:requested_sources) do
{
api_particulier_sources: { cnaf: { enfants: ['noms_prenoms'] } }
}
end
it 'saves the source' do
expect(procedure.api_particulier_sources).to eq("cnaf" => { "enfants" => ["noms_prenoms"] })
expect(flash.notice).to eq(I18n.t(".new_administrateur.sources_particulier.update.sources_ok"))
end
end
end
end

View file

@ -5,6 +5,32 @@ describe Users::ProfilController, type: :controller do
before { sign_in(user) }
describe 'GET #show' do
render_views
before { post :show }
context 'when the current user is not an instructeur' do
it { expect(response.body).to include(I18n.t('users.profil.show.transfer_title')) }
context 'when an existing transfer exists' do
let(:dossiers) { Array.new(3) { create(:dossier, user: user) } }
let(:next_owner) { 'loulou@lou.com' }
let!(:transfer) { DossierTransfer.initiate(next_owner, dossiers) }
before { post :show }
it { expect(response.body).to include(I18n.t('users.profil.show.one_waiting_transfer', count: dossiers.count, email: next_owner)) }
end
end
context 'when the current user is an instructeur' do
let(:user) { create(:instructeur).user }
it { expect(response.body).not_to include(I18n.t('users.profil.show.transfer_title')) }
end
end
describe 'POST #renew_api_token' do
let(:administrateur) { create(:administrateur) }
@ -72,4 +98,20 @@ describe Users::ProfilController, type: :controller do
it { expect(response).to redirect_to(profil_path) }
end
end
context 'POST #transfer_all_dossiers' do
let!(:dossiers) { Array.new(3) { create(:dossier, user: user) } }
let(:next_owner) { 'loulou@lou.com' }
let(:created_transfer) { DossierTransfer.first }
before do
post :transfer_all_dossiers, params: { next_owner: next_owner }
end
it "transfer all dossiers" do
expect(created_transfer.email).to eq(next_owner)
expect(created_transfer.dossiers).to eq(dossiers)
expect(flash.notice).to eq("Le transfert de 3 dossiers à #{next_owner} est en cours")
end
end
end

View file

@ -6,8 +6,8 @@ feature 'The routing', js: true do
let(:litteraire_user) { create(:user, password: password) }
before do
procedure.update(routing_enabled: true)
procedure.defaut_groupe_instructeur.instructeurs << administrateur.instructeur
Flipper.enable_actor(:administrateur_routage, administrateur.user)
end
scenario 'works' do
@ -32,14 +32,14 @@ feature 'The routing', js: true do
# add victor to littéraire groupe
find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
expect(page).to have_text("Linstructeur victor@inst.com a été affecté au groupe « littéraire »")
victor = User.find_by(email: 'victor@inst.com').instructeur
# add superwoman to littéraire groupe
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
expect(page).to have_text("Linstructeur superwoman@inst.com a été affecté au groupe « littéraire »")
superwoman = User.find_by(email: 'superwoman@inst.com').instructeur

View file

@ -98,4 +98,14 @@ feature 'Managing password:' do
expect(page).to have_content('Votre mot de passe a bien été modifié.')
end
end
scenario 'the password reset token has expired' do
visit edit_user_password_path(reset_password_token: 'invalid-password-token')
expect(page).to have_content 'Changement de mot de passe'
fill_in 'user_password', with: 'SomePassword'
fill_in 'user_password_confirmation', with: 'SomePassword'
click_on 'Changer le mot de passe'
expect(page).to have_content('Votre lien de nouveau mot de passe a expiré')
end
end

View file

@ -0,0 +1,44 @@
---
http_interactions:
- request:
method: get
uri: https://particulier.api.gouv.fr/api/introspect
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- demarches-simplifiees.fr
Accept:
- application/json
X-Api-Key:
- d7e9c9f4c3ca00caadde31f50fd4521a
Expect:
- ''
response:
status:
code: 200
message: OK
headers:
Date:
- Tue, 16 Mar 2021 15:25:24 GMT
Content-Type:
- application/json
Content-Length:
- '228'
Connection:
- keep-alive
Keep-Alive:
- timeout=5
X-Gravitee-Request-Id:
- 0e4dd327-de40-4052-8dd3-27de401052c4
X-Gravitee-Transaction-Id:
- cc30bb74-6516-46d9-b0bb-746516d6d904
Strict-Transport-Security:
- max-age=15552000
body:
encoding: UTF-8
string: '{"_id":"1d99db5a-a099-4314-ad2f-2707c6b505a6","name":"Application de
sandbox","scopes":["dgfip_avis_imposition","dgfip_adresse","cnaf_allocataires","cnaf_enfants","cnaf_adresse","cnaf_quotient_familial","mesri_statut_etudiant"]}'
recorded_at: Tue, 16 Mar 2021 15:25:24 GMT
recorded_with: VCR 6.0.0

View file

@ -0,0 +1,44 @@
---
http_interactions:
- request:
method: get
uri: https://particulier.api.gouv.fr/api/introspect
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- demarches-simplifiees.fr
Accept:
- application/json
X-Api-Key:
- d7e9c9f4c3ca00caadde31f50fd4521a
Expect:
- ''
response:
status:
code: 200
message: OK
headers:
Date:
- Tue, 16 Mar 2021 15:25:24 GMT
Content-Type:
- application/json
Content-Length:
- '228'
Connection:
- keep-alive
Keep-Alive:
- timeout=5
X-Gravitee-Request-Id:
- 0e4dd327-de40-4052-8dd3-27de401052c4
X-Gravitee-Transaction-Id:
- cc30bb74-6516-46d9-b0bb-746516d6d904
Strict-Transport-Security:
- max-age=15552000
body:
encoding: UTF-8
string: '{"_id":"1d99db5a-a099-4314-ad2f-2707c6b505a6","name":"Application de
sandbox","scopes":[]}'
recorded_at: Tue, 16 Mar 2021 15:25:24 GMT
recorded_with: VCR 6.0.0

View file

@ -0,0 +1,44 @@
---
http_interactions:
- request:
method: get
uri: https://particulier.api.gouv.fr/api/introspect
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- demarches-simplifiees.fr
Accept:
- application/json
X-Api-Key:
- d7e9c9f4c3ca00caadde31f50fd4521a
Expect:
- ''
response:
status:
code: 401
message: ''
headers:
Server:
- nginx
Date:
- Wed, 15 Sep 2021 10:02:12 GMT
Content-Type:
- application/json; charset=utf-8
Content-Length:
- '134'
X-Powered-By:
- Express
Vary:
- Origin
Etag:
- W/"86-FwFf7uuVKCSJkazn1ZHnY1yVYUo"
Strict-Transport-Security:
- max-age=15724800; includeSubdomains
body:
encoding: UTF-8
string: >
{"error":"acces_denied","reason":"Token not found or inactive","message":"Votre jeton d'API n'a pas été trouvé ou n'est pas actif"}
recorded_at: Wed, 15 Sep 2021 10:02:12 GMT
recorded_with: VCR 6.0.0

View file

@ -0,0 +1,28 @@
describe APIParticulier::API do
let(:token) { "d7e9c9f4c3ca00caadde31f50fd4521a" }
let(:api) { APIParticulier::API.new(token) }
before { stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") }
describe "scopes" do
subject { api.scopes }
it "doit retourner une liste de scopes" do
VCR.use_cassette("api_particulier/success/introspect") do
expect(subject).to match_array(['dgfip_avis_imposition', 'dgfip_adresse', 'cnaf_allocataires', 'cnaf_enfants', 'cnaf_adresse', 'cnaf_quotient_familial', 'mesri_statut_etudiant'])
end
end
it "returns an unauthorized exception" do
VCR.use_cassette("api_particulier/unauthorized/introspect") do
begin
subject
rescue APIParticulier::Error::Unauthorized => e
expect(e.message).to include('url: particulier.api.gouv.fr/api/introspect')
expect(e.message).to include('HTTP error code: 401')
expect(e.message).to include("Votre jeton d'API n'a pas été trouvé ou n'est pas actif")
end
end
end
end
end

View file

@ -0,0 +1,53 @@
describe APIParticulier::Services::SourcesService do
let(:service) { described_class.new(procedure) }
let(:procedure) { create(:procedure) }
let(:api_particulier_scopes) { [] }
let(:api_particulier_sources) { {} }
before do
procedure.update(api_particulier_scopes: api_particulier_scopes)
procedure.update(api_particulier_sources: api_particulier_sources)
end
describe "#available_sources" do
subject { service.available_sources }
context 'when the procedure doesnt have any available scopes' do
it { is_expected.to eq({}) }
end
context 'when a procedure has a cnaf_allocataires and a cnaf_adresse scopes' do
let(:api_particulier_scopes) { ['cnaf_allocataires', 'cnaf_enfants'] }
let(:cnaf_allocataires_and_enfants) do
{
'cnaf' => {
'allocataires' => ['noms_prenoms', 'date_de_naissance', 'sexe'],
'enfants' => ['noms_prenoms', 'date_de_naissance', 'sexe']
}
}
end
it { is_expected.to match(cnaf_allocataires_and_enfants) }
end
end
describe '#sanitize' do
subject { service.sanitize(requested_sources) }
let(:api_particulier_scopes) { ['cnaf_allocataires', 'cnaf_adresse'] }
let(:requested_sources) do
{
'cnaf' => {
'allocataires' => ['noms_prenoms', 'forbidden_sources', { 'weird_object' => 1 }],
'forbidden_scope' => ['any_source'],
'adresse' => { 'weird_object' => 1 }
},
'forbidden_provider' => { 'anything_scope' => ['any_source'] }
}
end
it { is_expected.to eq({ 'cnaf' => { 'allocataires' => ['noms_prenoms'] } }) }
end
end

View file

@ -21,7 +21,7 @@ describe Champs::PieceJustificativeChamp do
subject { champ_pj }
context "by default" do
it { is_expected.to validate_size_of(:piece_justificative_file).less_than(Champs::PieceJustificativeChamp::MAX_SIZE) }
it { is_expected.to validate_size_of(:piece_justificative_file).less_than(Champs::PieceJustificativeChamp::FILE_MAX_SIZE) }
it { is_expected.to validate_content_type_of(:piece_justificative_file).rejecting('application/x-ms-dos-executable') }
it { expect(champ_pj.type_de_champ.skip_pj_validation).to be_falsy }
end
@ -29,7 +29,7 @@ describe Champs::PieceJustificativeChamp do
context "when validation is disabled" do
before { champ_pj.type_de_champ.update(skip_pj_validation: true) }
it { is_expected.not_to validate_size_of(:piece_justificative_file).less_than(Champs::PieceJustificativeChamp::MAX_SIZE) }
it { is_expected.not_to validate_size_of(:piece_justificative_file).less_than(Champs::PieceJustificativeChamp::FILE_MAX_SIZE) }
end
context "when content-type validation is disabled" do

View file

@ -326,7 +326,7 @@ describe Procedure do
describe 'clone' do
let(:service) { create(:service) }
let(:procedure) { create(:procedure, received_mail: received_mail, service: service, types_de_champ: [type_de_champ_0, type_de_champ_1, type_de_champ_2, type_de_champ_pj, type_de_champ_repetition], types_de_champ_private: [type_de_champ_private_0, type_de_champ_private_1, type_de_champ_private_2, type_de_champ_private_repetition]) }
let(:procedure) { create(:procedure, received_mail: received_mail, service: service, types_de_champ: [type_de_champ_0, type_de_champ_1, type_de_champ_2, type_de_champ_pj, type_de_champ_repetition], types_de_champ_private: [type_de_champ_private_0, type_de_champ_private_1, type_de_champ_private_2, type_de_champ_private_repetition], api_particulier_token: '123456789012345', api_particulier_scopes: ['cnaf_famille']) }
let(:type_de_champ_0) { build(:type_de_champ, position: 0) }
let(:type_de_champ_1) { build(:type_de_champ, position: 1) }
let(:type_de_champ_2) { build(:type_de_champ_drop_down_list, position: 2) }
@ -471,6 +471,11 @@ describe Procedure do
expect(subject.groupe_instructeurs.where(label: "groupe_1").first).to be nil
end
it "should discard api_particulier_scopes and token" do
expect(subject.encrypted_api_particulier_token).to be_nil
expect(subject.api_particulier_scopes).to be_empty
end
it 'should have a default groupe instructeur' do
expect(subject.groupe_instructeurs.size).to eq(1)
expect(subject.groupe_instructeurs.first.label).to eq(GroupeInstructeur::DEFAUT_LABEL)