commit
282b3deb93
75 changed files with 1521 additions and 201 deletions
|
@ -359,7 +359,8 @@
|
|||
|
||||
.cnaf-inputs,
|
||||
.dgfip-inputs,
|
||||
.pole-emploi-inputs {
|
||||
.pole-emploi-inputs,
|
||||
.mesri-inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
|
30
app/assets/stylesheets/mesri.scss
Normal file
30
app/assets/stylesheets/mesri.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
@import "constants";
|
||||
@import "colors";
|
||||
|
||||
table.mesri {
|
||||
margin: 2 * $default-padding 0 $default-padding $default-padding;
|
||||
width: 100%;
|
||||
|
||||
caption {
|
||||
font-weight: bold;
|
||||
margin-left: - $default-padding;
|
||||
margin-bottom: $default-spacer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
font-weight: normal;
|
||||
padding: $default-spacer;
|
||||
}
|
||||
|
||||
th.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
th {
|
||||
border-bottom: 1px solid $grey;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ $stat-card-half-horizontal-spacing: 4 * $default-space;
|
|||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
width: 200px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-card-details {
|
||||
|
@ -132,6 +133,7 @@ $big-number-card-padding: 2 * $segmented-control-item-border-radius;
|
|||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
color: $light-grey;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.long-title {
|
||||
margin-left: -30px;
|
||||
|
|
|
@ -27,8 +27,8 @@ class StatsController < ApplicationController
|
|||
"Terminé" => stat.dossiers_termines
|
||||
}
|
||||
|
||||
@procedures_cumulative = cumulative_hash(procedures, :published_at)
|
||||
@procedures_in_the_last_4_months = last_four_months_hash(procedures, :published_at)
|
||||
@procedures_cumulative = cumulative_month_serie(procedures, :published_at)
|
||||
@procedures_in_the_last_4_months = last_four_months_serie(procedures, :published_at)
|
||||
|
||||
@dossiers_cumulative = stat.dossiers_cumulative
|
||||
@dossiers_in_the_last_4_months = stat.dossiers_in_the_last_4_months
|
||||
|
@ -57,9 +57,9 @@ class StatsController < ApplicationController
|
|||
"procedures.libelle",
|
||||
"users.id",
|
||||
"dossiers.state",
|
||||
"dossiers.depose_at - dossiers.created_at",
|
||||
"dossiers.en_instruction_at - dossiers.depose_at",
|
||||
"dossiers.processed_at - dossiers.en_instruction_at"
|
||||
Arel.sql("dossiers.depose_at - dossiers.created_at"),
|
||||
Arel.sql("dossiers.en_instruction_at - dossiers.depose_at"),
|
||||
Arel.sql("dossiers.processed_at - dossiers.en_instruction_at")
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -136,27 +136,18 @@ class StatsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def last_four_months_hash(association, date_attribute)
|
||||
min_date = 3.months.ago.beginning_of_month.to_date
|
||||
|
||||
def last_four_months_serie(association, date_attribute)
|
||||
association
|
||||
.where(date_attribute => min_date..max_date)
|
||||
.group("DATE_TRUNC('month', #{date_attribute})")
|
||||
.group_by_month(date_attribute, last: 4, current: super_admin_signed_in?)
|
||||
.count
|
||||
.to_a
|
||||
.sort_by { |a| a[0] }
|
||||
.map { |e| [I18n.l(e.first, format: "%B %Y"), e.last] }
|
||||
.transform_keys { |date| l(date, format: "%B %Y") }
|
||||
end
|
||||
|
||||
def cumulative_hash(association, date_attribute)
|
||||
def cumulative_month_serie(association, date_attribute)
|
||||
sum = 0
|
||||
association
|
||||
.where("#{date_attribute} < ?", max_date)
|
||||
.group("DATE_TRUNC('month', #{date_attribute})")
|
||||
.group_by_month(date_attribute, current: super_admin_signed_in?)
|
||||
.count
|
||||
.to_a
|
||||
.sort_by { |a| a[0] }
|
||||
.map { |x, y| { x => (sum += y) } }
|
||||
.reduce({}, :merge)
|
||||
.transform_values { |count| sum += count }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -348,8 +348,8 @@ module Users
|
|||
def champs_params
|
||||
params.permit(dossier: {
|
||||
champs_attributes: [
|
||||
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :piece_justificative_file, :departement, :code_departement, value: [],
|
||||
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :piece_justificative_file, :departement, :code_departement, value: []]
|
||||
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: [],
|
||||
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: []]
|
||||
]
|
||||
})
|
||||
end
|
||||
|
|
|
@ -19,7 +19,11 @@ class Users::SessionsController < Devise::SessionsController
|
|||
end
|
||||
|
||||
def link_sent
|
||||
if Devise.email_regexp.match?(params[:email])
|
||||
@email = params[:email]
|
||||
else
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /resource/sign_out
|
||||
|
|
|
@ -1983,6 +1983,11 @@ enum TypeDeChamp {
|
|||
"""
|
||||
linked_drop_down_list
|
||||
|
||||
"""
|
||||
Données du Ministère de l'Enseignement Supérieur, de la Recherche et de l'Innovation
|
||||
"""
|
||||
mesri
|
||||
|
||||
"""
|
||||
Choix multiples
|
||||
"""
|
||||
|
|
9
app/helpers/zone_helper.rb
Normal file
9
app/helpers/zone_helper.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module ZoneHelper
|
||||
def grouped_options_for_zone
|
||||
collectivite = Zone.find_by(acronym: "COLLECTIVITE")
|
||||
{
|
||||
"--" => [[I18n.t('i_dont_know', scope: 'utils'), nil], [collectivite.label, collectivite.id]],
|
||||
I18n.t('ministeres', scope: 'zones') => (Zone.order(:label) - [collectivite]).map { |m| [m.label, m.id] }
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
class ArchiveCreationJob < ApplicationJob
|
||||
queue_as :exports
|
||||
queue_as :archives
|
||||
|
||||
def perform(procedure, archive, instructeur)
|
||||
ProcedureArchiveService
|
||||
|
|
|
@ -1,4 +1,20 @@
|
|||
class ActiveStorage::DownloadableFile
|
||||
# https://edgeapi.rubyonrails.org/classes/ActiveStorage/Blob.html#method-i-download
|
||||
def self.download(attachment:, destination_path:, in_chunk: true)
|
||||
byte_written = 0
|
||||
|
||||
File.open(destination_path, mode: 'wb') do |fd| # we expact a path as string, so we can recreate the file (ex: failure/retry on former existing fd)
|
||||
if in_chunk
|
||||
attachment.download do |chunk|
|
||||
byte_written += fd.write(chunk)
|
||||
end
|
||||
else
|
||||
byte_written = fd.write(attachment.download)
|
||||
end
|
||||
end
|
||||
byte_written
|
||||
end
|
||||
|
||||
def self.create_list_from_dossier(dossier, for_expert = false)
|
||||
dossier_export = PiecesJustificativesService.generate_dossier_export(dossier)
|
||||
pjs = [dossier_export] + PiecesJustificativesService.liste_documents(dossier, for_expert)
|
||||
|
|
|
@ -4,7 +4,8 @@ class APIParticulier::API
|
|||
INTROSPECT_RESOURCE_NAME = "introspect"
|
||||
COMPOSITION_FAMILIALE_RESOURCE_NAME = "v2/composition-familiale"
|
||||
AVIS_IMPOSITION_RESOURCE_NAME = "v2/avis-imposition"
|
||||
SITUATION_POLE_EMPLOI = "v2/situations-pole-emploi"
|
||||
SITUATION_POLE_EMPLOI_RESOURCE_NAME = "v2/situations-pole-emploi"
|
||||
ETUDIANTS_RESOURCE_NAME = "v2/etudiants"
|
||||
|
||||
TIMEOUT = 20
|
||||
|
||||
|
@ -31,7 +32,17 @@ class APIParticulier::API
|
|||
end
|
||||
|
||||
def situation_pole_emploi(identifiant)
|
||||
get(SITUATION_POLE_EMPLOI, identifiant: identifiant)
|
||||
get(SITUATION_POLE_EMPLOI_RESOURCE_NAME, identifiant: identifiant)
|
||||
end
|
||||
|
||||
def etudiants(ine)
|
||||
# NOTE: Paramètres d'appel mutuellement exclusifs,
|
||||
# l'appel par INE est réservé aux acteurs de la sphère de l'enseignement
|
||||
# - INE, l'Identifiant National Étudiant
|
||||
# - état civil, constitué du nom, prénom, date de naissance, sexe et lieu de naissance
|
||||
|
||||
# TODO: ajouter le support de l'état civil
|
||||
get(ETUDIANTS_RESOURCE_NAME, ine: ine)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
48
app/lib/api_particulier/mesri_adapter.rb
Normal file
48
app/lib/api_particulier/mesri_adapter.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
class APIParticulier::MesriAdapter
|
||||
class InvalidSchemaError < ::StandardError
|
||||
def initialize(errors)
|
||||
super(errors.map(&:to_json).join("\n"))
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(api_particulier_token, ine, requested_sources)
|
||||
@api = APIParticulier::API.new(api_particulier_token)
|
||||
@ine = ine
|
||||
@requested_sources = requested_sources
|
||||
end
|
||||
|
||||
def to_params
|
||||
@api.etudiants(@ine)
|
||||
.tap { |d| ensure_valid_schema!(d) }
|
||||
.then { |d| extract_requested_sources(d) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_valid_schema!(data)
|
||||
if !schemer.valid?(data)
|
||||
errors = schemer.validate(data).to_a
|
||||
raise InvalidSchemaError.new(errors)
|
||||
end
|
||||
end
|
||||
|
||||
def schemer
|
||||
@schemer ||= JSONSchemer.schema(Rails.root.join('app/schemas/etudiants.json'))
|
||||
end
|
||||
|
||||
def extract_requested_sources(data)
|
||||
@requested_sources['mesri']&.map do |(scope, sources)|
|
||||
case scope
|
||||
when 'inscriptions'
|
||||
{ scope => data[scope].filter_map { |d| d.slice(*sources) if d.key?('dateDebutInscription') } }
|
||||
when 'admissions'
|
||||
{ scope => data['inscriptions'].filter_map { |d| d.slice(*sources) if d.key?('dateDebutAdmission') } }
|
||||
when 'etablissements'
|
||||
{ scope => data['inscriptions'].map { |d| d['etablissement'].slice(*sources) }.uniq }
|
||||
else
|
||||
{ scope => data.slice(*sources) }
|
||||
end
|
||||
end
|
||||
&.flatten&.reduce(&:deep_merge) || {}
|
||||
end
|
||||
end
|
|
@ -8,6 +8,7 @@ module APIParticulier
|
|||
def available_sources
|
||||
@procedure.api_particulier_scopes
|
||||
.filter_map { |provider_and_scope| raw_scopes[provider_and_scope] }
|
||||
.uniq # remove provider/scope tuples duplicates (e.g. mesri inscriptions)
|
||||
.map { |provider, scope| extract_sources(provider, scope) }
|
||||
.reduce({}) { |acc, el| acc.deep_merge(el) { |_, this_val, other_val| this_val + other_val } }
|
||||
end
|
||||
|
@ -80,7 +81,13 @@ module APIParticulier
|
|||
'pole_emploi_identite' => ['pole_emploi', 'identite'],
|
||||
'pole_emploi_adresse' => ['pole_emploi', 'adresse'],
|
||||
'pole_emploi_contact' => ['pole_emploi', 'contact'],
|
||||
'pole_emploi_inscription' => ['pole_emploi', 'inscription']
|
||||
'pole_emploi_inscription' => ['pole_emploi', 'inscription'],
|
||||
'mesri_identifiant' => ['mesri', 'identifiant'],
|
||||
'mesri_identite' => ['mesri', 'identite'],
|
||||
'mesri_inscription_etudiant' => ['mesri', 'inscriptions'],
|
||||
'mesri_inscription_autre' => ['mesri', 'inscriptions'],
|
||||
'mesri_admission' => ['mesri', 'admissions'],
|
||||
'mesri_etablissements' => ['mesri', 'etablissements']
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -123,6 +130,13 @@ module APIParticulier
|
|||
'adresse' => ['INSEECommune', 'codePostal', 'localite', 'ligneVoie', 'ligneComplementDestinataire', 'ligneComplementAdresse', 'ligneComplementDistribution', 'ligneNom'],
|
||||
'contact' => ['email', 'telephone', 'telephone2'],
|
||||
'inscription' => ['dateInscription', 'dateCessationInscription', 'codeCertificationCNAV', 'codeCategorieInscription', 'libelleCategorieInscription']
|
||||
},
|
||||
'mesri' => {
|
||||
'identifiant' => ['ine'],
|
||||
'identite' => ['nom', 'prenom', 'dateNaissance'],
|
||||
'inscriptions' => ['statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune'],
|
||||
'admissions' => ['statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune'],
|
||||
'etablissements' => ['uai', 'nom']
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
20
app/lib/utils/retryable.rb
Normal file
20
app/lib/utils/retryable.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
module Utils
|
||||
module Retryable
|
||||
# usage:
|
||||
# max_attempt : retry count
|
||||
# errors : only retry those errors
|
||||
# with_retry(max_attempt: 10, errors: [StandardError]) do
|
||||
# do_something_which_can_fail
|
||||
# end
|
||||
def with_retry(max_attempt: 1, errors: [StandardError], &block)
|
||||
limiter = 0
|
||||
begin
|
||||
yield
|
||||
rescue *errors
|
||||
limiter += 1
|
||||
retry if limiter <= max_attempt
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
47
app/models/champs/mesri_champ.rb
Normal file
47
app/models/champs/mesri_champ.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: champs
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# data :jsonb
|
||||
# fetch_external_data_exceptions :string is an Array
|
||||
# private :boolean default(FALSE), not null
|
||||
# rebased_at :datetime
|
||||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
# etablissement_id :integer
|
||||
# external_id :string
|
||||
# parent_id :bigint
|
||||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::MesriChamp < Champs::TextChamp
|
||||
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/mesri-input-validation.middleware.ts
|
||||
store_accessor :value_json, :ine
|
||||
|
||||
def blank?
|
||||
external_id.nil?
|
||||
end
|
||||
|
||||
def fetch_external_data?
|
||||
true
|
||||
end
|
||||
|
||||
def fetch_external_data
|
||||
return if !valid?
|
||||
|
||||
APIParticulier::MesriAdapter.new(
|
||||
procedure.api_particulier_token,
|
||||
ine,
|
||||
procedure.api_particulier_sources
|
||||
).to_params
|
||||
end
|
||||
|
||||
def external_id
|
||||
{ ine: ine }.to_json if ine.present?
|
||||
end
|
||||
end
|
|
@ -15,6 +15,7 @@ class GroupeInstructeur < ApplicationRecord
|
|||
has_many :instructeurs, through: :assign_tos
|
||||
has_many :dossiers
|
||||
has_and_belongs_to_many :exports, dependent: :destroy
|
||||
has_and_belongs_to_many :bulk_messages, dependent: :destroy
|
||||
|
||||
validates :label, presence: { message: 'doit être renseigné' }, allow_nil: false
|
||||
validates :label, uniqueness: { scope: :procedure, message: 'existe déjà' }
|
||||
|
|
|
@ -734,6 +734,10 @@ class Procedure < ApplicationRecord
|
|||
api_particulier_sources['pole_emploi'].present?
|
||||
end
|
||||
|
||||
def mesri_enabled?
|
||||
api_particulier_sources['mesri'].present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_for_publication?
|
||||
|
|
|
@ -30,11 +30,11 @@ class Stat < ApplicationRecord
|
|||
dossiers_deposes_entre_60_et_30_jours: states['dossiers_deposes_entre_60_et_30_jours'],
|
||||
dossiers_not_brouillon: states['not_brouillon'],
|
||||
dossiers_termines: states['termines'],
|
||||
dossiers_cumulative: cumulative_hash([
|
||||
dossiers_cumulative: cumulative_month_serie([
|
||||
[Dossier.state_not_brouillon, :depose_at],
|
||||
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
||||
]),
|
||||
dossiers_in_the_last_4_months: last_four_months_hash([
|
||||
dossiers_in_the_last_4_months: last_four_months_serie([
|
||||
[Dossier.state_not_brouillon, :depose_at],
|
||||
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
||||
]),
|
||||
|
@ -75,39 +75,33 @@ class Stat < ApplicationRecord
|
|||
)
|
||||
end
|
||||
|
||||
def last_four_months_hash(associations_with_date_attribute)
|
||||
min_date = 3.months.ago.beginning_of_month.to_date
|
||||
def last_four_months_serie(associations_with_date_attribute)
|
||||
timeseries = associations_with_date_attribute.map do |association, date_attribute|
|
||||
association
|
||||
.where(date_attribute => min_date..max_date)
|
||||
.group("DATE_TRUNC('month', #{date_attribute})")
|
||||
.count
|
||||
association.group_by_month(date_attribute, last: 4, current: false).count
|
||||
end
|
||||
|
||||
sum_hashes(*timeseries)
|
||||
.to_a
|
||||
.sort_by { |a| a[0] }
|
||||
.map { |e| [I18n.l(e.first, format: "%B %Y"), e.last] }
|
||||
month_serie(sum_hashes(*timeseries))
|
||||
end
|
||||
|
||||
def cumulative_hash(associations_with_date_attribute)
|
||||
def month_serie(date_serie)
|
||||
date_serie.keys.sort.each_with_object({}) { |date, h| h[I18n.l(date, format: "%B %Y")] = date_serie[date] }
|
||||
end
|
||||
|
||||
def cumulative_month_serie(associations_with_date_attribute)
|
||||
timeseries = associations_with_date_attribute.map do |association, date_attribute|
|
||||
association
|
||||
.where("#{date_attribute} < ?", max_date)
|
||||
.group("DATE_TRUNC('month', #{date_attribute})")
|
||||
.count
|
||||
association.group_by_month(date_attribute, current: false).count
|
||||
end
|
||||
|
||||
cumulative_serie(sum_hashes(*timeseries))
|
||||
end
|
||||
|
||||
def cumulative_serie(sums)
|
||||
sum = 0
|
||||
sum_hashes(*timeseries)
|
||||
.to_a
|
||||
.sort_by { |a| a[0] }
|
||||
.map { |x, y| { x => (sum += y) } }
|
||||
.reduce({}, :merge)
|
||||
sums.keys.sort.index_with { |date| sum += sums[date] }
|
||||
end
|
||||
|
||||
def sum_hashes(*hashes)
|
||||
{}.merge(*hashes) { |_k, hash_one_value, hash_two_value| hash_one_value + hash_two_value }
|
||||
{}.merge(*hashes) { |_k, v1, v2| v1 + v2 }
|
||||
end
|
||||
|
||||
def max_date
|
||||
|
|
|
@ -43,9 +43,9 @@ class Traitement < ApplicationRecord
|
|||
.to_sql
|
||||
|
||||
sql = <<~EOF
|
||||
select date_trunc('month', r1.processed_at) as month, count(r1.processed_at)
|
||||
select date_trunc('month', r1.processed_at::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL) as month, count(r1.processed_at)
|
||||
from (#{last_traitements_per_dossier}) as r1
|
||||
group by date_trunc('month', r1.processed_at)
|
||||
group by date_trunc('month', r1.processed_at::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)
|
||||
order by month desc
|
||||
EOF
|
||||
|
||||
|
|
|
@ -52,7 +52,8 @@ class TypeDeChamp < ApplicationRecord
|
|||
annuaire_education: 'annuaire_education',
|
||||
cnaf: 'cnaf',
|
||||
dgfip: 'dgfip',
|
||||
pole_emploi: 'pole_emploi'
|
||||
pole_emploi: 'pole_emploi',
|
||||
mesri: 'mesri'
|
||||
}
|
||||
|
||||
belongs_to :revision, class_name: 'ProcedureRevision', optional: true
|
||||
|
@ -330,6 +331,8 @@ class TypeDeChamp < ApplicationRecord
|
|||
procedure.dgfip_enabled?
|
||||
when TypeDeChamp.type_champs.fetch(:pole_emploi)
|
||||
procedure.pole_emploi_enabled?
|
||||
when TypeDeChamp.type_champs.fetch(:mesri)
|
||||
procedure.mesri_enabled?
|
||||
else
|
||||
true
|
||||
end
|
||||
|
|
2
app/models/types_de_champ/mesri_type_de_champ.rb
Normal file
2
app/models/types_de_champ/mesri_type_de_champ.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class TypesDeChamp::MesriTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
end
|
57
app/schemas/etudiants.json
Normal file
57
app/schemas/etudiants.json
Normal file
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "http://demarches-simplifiees.fr/etudiants.schema.json",
|
||||
"title": "statut étudiant",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ine": {
|
||||
"type": "string"
|
||||
},
|
||||
"nom": {
|
||||
"type": "string"
|
||||
},
|
||||
"prenom": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateNaissance": {
|
||||
"format": "date",
|
||||
"type": "string"
|
||||
},
|
||||
"inscriptions": {
|
||||
"type": "array",
|
||||
"items" : {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dateDebutInscription": {
|
||||
"format": "date",
|
||||
"type": "string"
|
||||
},
|
||||
"dateFinInscription": {
|
||||
"format": "date",
|
||||
"type": "string"
|
||||
},
|
||||
"statut": {
|
||||
"enum": ["admis", "inscrit"]
|
||||
},
|
||||
"regime": {
|
||||
"enum": ["formation initiale", "formation continue"]
|
||||
},
|
||||
"codeCommune": {
|
||||
"type": "string"
|
||||
},
|
||||
"etablissement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uai": {
|
||||
"type": "string"
|
||||
},
|
||||
"nom": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,6 @@
|
|||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"$defs": {
|
||||
"adresse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -67,7 +66,6 @@
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateInscription": {
|
||||
"format": "date",
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
require 'tempfile'
|
||||
|
||||
class ProcedureArchiveService
|
||||
include Utils::Retryable
|
||||
ARCHIVE_CREATION_DIR = ENV.fetch('ARCHIVE_CREATION_DIR') { '/tmp' }
|
||||
|
||||
def initialize(procedure)
|
||||
@procedure = procedure
|
||||
end
|
||||
|
@ -14,6 +17,35 @@ class ProcedureArchiveService
|
|||
end
|
||||
|
||||
def collect_files_archive(archive, instructeur)
|
||||
if Flipper.enabled?(:zip_using_binary, @procedure)
|
||||
new_collect_files_archive(archive, instructeur)
|
||||
else
|
||||
old_collect_files_archive(archive, instructeur)
|
||||
end
|
||||
end
|
||||
|
||||
def new_collect_files_archive(archive, instructeur)
|
||||
## faux, ca ne doit prendre que certains groupe instructeur ?
|
||||
if archive.time_span_type == 'everything'
|
||||
dossiers = @procedure.dossiers.state_termine
|
||||
else
|
||||
dossiers = @procedure.dossiers.processed_in_month(archive.month)
|
||||
end
|
||||
|
||||
attachments = create_list_of_attachments(dossiers)
|
||||
download_and_zip(attachments) do |zip_file|
|
||||
archive.file.attach(
|
||||
io: File.open(zip_file),
|
||||
filename: archive.filename(@procedure),
|
||||
# we don't want to run virus scanner on this file
|
||||
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
)
|
||||
end
|
||||
archive.make_available!
|
||||
InstructeurMailer.send_archive(instructeur, @procedure, archive).deliver_later
|
||||
end
|
||||
|
||||
def old_collect_files_archive(archive, instructeur)
|
||||
if archive.time_span_type == 'everything'
|
||||
dossiers = @procedure.dossiers.state_termine
|
||||
else
|
||||
|
@ -27,7 +59,7 @@ class ProcedureArchiveService
|
|||
Zip::OutputStream.open(tmp_file) do |zipfile|
|
||||
bug_reports = ''
|
||||
files.each do |attachment, pj_filename|
|
||||
zipfile.put_next_entry("#{zip_root_folder(@procedure)}/#{pj_filename}")
|
||||
zipfile.put_next_entry("#{zip_root_folder}/#{pj_filename}")
|
||||
begin
|
||||
zipfile.puts(attachment.download)
|
||||
rescue
|
||||
|
@ -35,7 +67,7 @@ class ProcedureArchiveService
|
|||
end
|
||||
end
|
||||
if !bug_reports.empty?
|
||||
zipfile.put_next_entry("#{zip_root_folder(@procedure)}/LISEZMOI.txt")
|
||||
zipfile.put_next_entry("#{zip_root_folder}/LISEZMOI.txt")
|
||||
zipfile.puts(bug_reports)
|
||||
end
|
||||
end
|
||||
|
@ -63,7 +95,51 @@ class ProcedureArchiveService
|
|||
|
||||
private
|
||||
|
||||
def zip_root_folder(procedure)
|
||||
def download_and_zip(attachments, &block)
|
||||
Dir.mktmpdir(nil, ARCHIVE_CREATION_DIR) do |tmp_dir|
|
||||
archive_dir = File.join(tmp_dir, zip_root_folder)
|
||||
zip_path = File.join(ARCHIVE_CREATION_DIR, "#{zip_root_folder}.zip")
|
||||
|
||||
begin
|
||||
FileUtils.remove_entry_secure(archive_dir) if Dir.exist?(archive_dir)
|
||||
Dir.mkdir(archive_dir)
|
||||
|
||||
bug_reports = ''
|
||||
attachments.each do |attachment, path|
|
||||
attachment_path = File.join(archive_dir, path)
|
||||
attachment_dir = File.dirname(attachment_path)
|
||||
|
||||
FileUtils.mkdir_p(attachment_dir) if !Dir.exist?(attachment_dir)
|
||||
begin
|
||||
with_retry(max_attempt: 1) do
|
||||
ActiveStorage::DownloadableFile.download(attachment: attachment,
|
||||
destination_path: attachment_path,
|
||||
in_chunk: true)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error("Fail to download filename #{File.basename(attachment_path)} in procedure##{@procedure.id}, reason: #{e}")
|
||||
File.delete(attachment_path) if File.exist?(attachment_path)
|
||||
bug_reports += "Impossible de récupérer le fichier #{File.basename(attachment_path)}\n"
|
||||
end
|
||||
end
|
||||
|
||||
if !bug_reports.empty?
|
||||
File.write(File.join(archive_dir, 'LISEZMOI.txt'), bug_reports)
|
||||
end
|
||||
|
||||
File.delete(zip_path) if File.exist?(zip_path)
|
||||
Dir.chdir(tmp_dir) do
|
||||
system 'zip', '-r', zip_path, zip_root_folder
|
||||
end
|
||||
yield(zip_path)
|
||||
ensure
|
||||
FileUtils.remove_entry_secure(archive_dir) if Dir.exist?(archive_dir)
|
||||
File.delete(zip_path) if File.exist?(zip_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def zip_root_folder
|
||||
"procedure-#{@procedure.id}"
|
||||
end
|
||||
|
||||
|
|
21
app/services/update_zone_to_procedures_service.rb
Normal file
21
app/services/update_zone_to_procedures_service.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class UpdateZoneToProceduresService
|
||||
def self.call(lines)
|
||||
errors = []
|
||||
lines.each do |line|
|
||||
zone_label = line["POL_PUB_MINISTERE RATTACHEMENT"]
|
||||
zone = Zone.find_by(acronym: zone_label)
|
||||
if zone.nil?
|
||||
errors << "Zone #{zone_label} introuvable"
|
||||
else
|
||||
id = line["id"]
|
||||
procedure = Procedure.find_by(id: id)
|
||||
if procedure
|
||||
procedure.update(zone: zone)
|
||||
else
|
||||
errors << "Procedure #{id} introuvable"
|
||||
end
|
||||
end
|
||||
end
|
||||
errors
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
class MonAvisEmbedValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
# We need to ensure the embed code is not any random string in order to avoid injections
|
||||
r = Regexp.new('<a href="https://monavis|voxusagers.numerique.gouv.fr/Demarches/\d+.*key=[[:alnum:]]+.*">\s*<img src="(https://monavis.numerique.gouv.fr/monavis-static/bouton-blanc|bleu.png)|(https://voxusagers.numerique.gouv.fr/static/bouton-(bleu|blanc).svg)" alt="Je donne mon avis" title="Je donne mon avis sur cette démarche" />\s*</a>', Regexp::MULTILINE)
|
||||
r = Regexp.new('<a href="https://monavis|voxusagers.numerique.gouv.fr/Demarches/\d+.*key=[[:alnum:]]+.*">\s*<img src="https://monavis|voxusagers.numerique.gouv.fr/(monavis-)?static/bouton-blanc|bleu.png|svg" alt="Je donne mon avis" title="Je donne mon avis sur cette démarche" />\s*</a>', Regexp::MULTILINE)
|
||||
if record.monavis_embed.present? && !r.match?(record.monavis_embed)
|
||||
record.errors[:base] << "Le code fourni ne correspond pas au format des codes MonAvis reconnus par la plateforme."
|
||||
end
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
%span.mandatory *
|
||||
= f.text_area :description, rows: '6', placeholder: 'Description de la démarche, destinataires, etc. ', class: 'form-control'
|
||||
|
||||
= f.label :zone do
|
||||
- if Flipper.enabled? :zonage
|
||||
= f.label :zone do
|
||||
= t('zone', scope: 'activerecord.attributes.procedure')
|
||||
%span.mandatory *
|
||||
= f.collection_select :zone_id, Zone.order(:label), :id, :label, prompt: true
|
||||
= f.select :zone_id, grouped_options_for_zone
|
||||
|
||||
%h3.header-subsection Logo de la démarche
|
||||
= image_upload_and_render f, @procedure.logo
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
.dropdown-description
|
||||
%h4 Clore
|
||||
|
||||
- if procedure.brouillon?
|
||||
- if procedure.can_be_deleted_by_administrateur?
|
||||
%li
|
||||
= link_to admin_procedure_path(procedure), method: :delete, data: { confirm: "Voulez-vous vraiment supprimer la démarche ? \nToute suppression est définitive et s'appliquera aux éventuels autres administrateurs de cette démarche !" } do
|
||||
%span.icon.refuse
|
||||
|
|
11
app/views/shared/champs/mesri/_admissions.html.haml
Normal file
11
app/views/shared/champs/mesri/_admissions.html.haml
Normal file
|
@ -0,0 +1,11 @@
|
|||
%table.mesri
|
||||
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
|
||||
- admissions.each do |admission|
|
||||
- admission.slice('statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune').keys.each do |key|
|
||||
%tr
|
||||
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
|
||||
- case key
|
||||
- when 'dateDebutAdmission', 'dateFinAdmission'
|
||||
%td= try_format_date(Date.strptime(admission[key], "%Y-%m-%d"))
|
||||
- else
|
||||
%td= admission[key]
|
7
app/views/shared/champs/mesri/_etablissements.html.haml
Normal file
7
app/views/shared/champs/mesri/_etablissements.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
%table.mesri
|
||||
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
|
||||
- etablissements.each do |etablissement|
|
||||
- etablissement.slice('uai', 'nom').keys.each do |key|
|
||||
%tr
|
||||
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
|
||||
%td= etablissement[key]
|
6
app/views/shared/champs/mesri/_identifiant.html.haml
Normal file
6
app/views/shared/champs/mesri/_identifiant.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
%table.mesri
|
||||
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
|
||||
- identifiant.slice('ine').keys.each do |key|
|
||||
%tr
|
||||
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
|
||||
%td= identifiant[key]
|
10
app/views/shared/champs/mesri/_identite.html.haml
Normal file
10
app/views/shared/champs/mesri/_identite.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%table.mesri
|
||||
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
|
||||
- identite.slice('nom', 'prenom', 'dateNaissance').keys.each do |key|
|
||||
%tr
|
||||
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
|
||||
- case key
|
||||
- when 'dateNaissance'
|
||||
%td= try_format_date(Date.strptime(identite[key], "%Y-%m-%d"))
|
||||
- else
|
||||
%td= identite[key]
|
11
app/views/shared/champs/mesri/_inscriptions.html.haml
Normal file
11
app/views/shared/champs/mesri/_inscriptions.html.haml
Normal file
|
@ -0,0 +1,11 @@
|
|||
%table.mesri
|
||||
%caption #{t("api_particulier.providers.mesri.scopes.#{scope}.libelle")} :
|
||||
- inscriptions.each do |inscription|
|
||||
- inscription.slice('statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune').keys.each do |key|
|
||||
%tr
|
||||
%th= t("api_particulier.providers.mesri.scopes.#{scope}.#{key}")
|
||||
- case key
|
||||
- when 'dateDebutInscription', 'dateFinInscription'
|
||||
%td= try_format_date(Date.strptime(inscription[key], "%Y-%m-%d"))
|
||||
- else
|
||||
%td= inscription[key]
|
25
app/views/shared/champs/mesri/_show.html.haml
Normal file
25
app/views/shared/champs/mesri/_show.html.haml
Normal file
|
@ -0,0 +1,25 @@
|
|||
- if champ.blank?
|
||||
%p= t('.not_filled')
|
||||
- elsif champ.data.blank?
|
||||
%p= t('.fetching_data', ine: champ.ine)
|
||||
- else
|
||||
- if profile == 'usager'
|
||||
- sources = champ.procedure.api_particulier_sources['mesri'].keys
|
||||
- i18n_sources = sources.map { |s| I18n.t("#{s}.libelle", scope: 'api_particulier.providers.mesri.scopes') }
|
||||
%p= t('.data_fetched', sources: i18n_sources.to_sentence, ine: champ.ine)
|
||||
|
||||
- if profile == 'instructeur'
|
||||
%p= t('.data_fetched_title')
|
||||
|
||||
- champ.data.slice('identifiant', 'identite', 'inscriptions', 'admissions', 'etablissements').keys.each do |scope|
|
||||
- case scope
|
||||
- when 'identifiant'
|
||||
= render partial: 'shared/champs/mesri/identifiant', locals: { scope: scope, identifiant: champ.data[scope] }
|
||||
- when 'identite'
|
||||
= render partial: 'shared/champs/mesri/identite', locals: { scope: scope, identite: champ.data[scope] }
|
||||
- when 'inscriptions'
|
||||
= render partial: 'shared/champs/mesri/inscriptions', locals: { scope: scope, inscriptions: champ.data[scope] }
|
||||
- when 'admissions'
|
||||
= render partial: 'shared/champs/mesri/admissions', locals: { scope: scope, admissions: champ.data[scope] }
|
||||
- when 'etablissements'
|
||||
= render partial: 'shared/champs/mesri/etablissements', locals: { scope: scope, etablissements: champ.data[scope] }
|
|
@ -42,6 +42,8 @@
|
|||
= render partial: "shared/champs/dgfip/show", locals: { champ: c, profile: profile }
|
||||
- when TypeDeChamp.type_champs.fetch(:pole_emploi)
|
||||
= render partial: "shared/champs/pole_emploi/show", locals: { champ: c, profile: profile }
|
||||
- when TypeDeChamp.type_champs.fetch(:mesri)
|
||||
= render partial: "shared/champs/mesri/show", locals: { champ: c, profile: profile }
|
||||
- when TypeDeChamp.type_champs.fetch(:address)
|
||||
= render partial: "shared/champs/address/show", locals: { champ: c }
|
||||
- when TypeDeChamp.type_champs.fetch(:communes)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.mesri-inputs
|
||||
%div
|
||||
= form.label :ine, t('.ine_label')
|
||||
%p.notice= t('.ine_notice')
|
||||
= form.text_field :ine,
|
||||
required: champ.mandatory?,
|
||||
aria: { describedby: describedby_id(champ) }
|
|
@ -7,45 +7,45 @@
|
|||
.stat-cards
|
||||
- if @usual_traitement_time.present?
|
||||
.stat-card.big-number-card
|
||||
%span.big-number-card-title TEMPS DE TRAITEMENT USUEL
|
||||
%span.big-number-card-title= t('.usual_processing_time')
|
||||
%span.big-number-card-number
|
||||
= distance_of_time_in_words(@usual_traitement_time)
|
||||
%span.big-number-card-detail
|
||||
#{ProcedureStatsConcern::USUAL_TRAITEMENT_TIME_PERCENTILE}% des demandes des #{ProcedureStatsConcern::NB_DAYS_RECENT_DOSSIERS} derniers jours ont été traitées en moins de #{distance_of_time_in_words(@usual_traitement_time)}.
|
||||
= t('.processing_time_description', percentile: ProcedureStatsConcern::USUAL_TRAITEMENT_TIME_PERCENTILE, span: ProcedureStatsConcern::NB_DAYS_RECENT_DOSSIERS, days: distance_of_time_in_words(@usual_traitement_time))
|
||||
|
||||
.stat-cards
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title TEMPS DE TRAITEMENT
|
||||
.stat-card-details depuis le lancement de la procédure
|
||||
%span.stat-card-title= t('.processing_time')
|
||||
.stat-card-details= t('.since_procedure_creation')
|
||||
.chart-container
|
||||
.chart
|
||||
- colors = %w(#C3D9FF #0069CC #1C7EC9) # from _colors.scss
|
||||
= column_chart @usual_traitement_time_by_month, ytitle: "Nb Jours", legend: "bottom", label: "Temps de traitement entre le passage en instruction et la réponse (accepté, refusé, ou classé sans suite) pour 90% des dossiers"
|
||||
= column_chart @usual_traitement_time_by_month, ytitle: t('.nb_days'), legend: "bottom", label: t('.processing_time_graph_description')
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title AVANCÉE DES DOSSIERS
|
||||
.stat-card-details depuis le lancement de la procédure
|
||||
%span.stat-card-title= t('.status_evolution')
|
||||
.stat-card-details= t('.status_evolution_details')
|
||||
.chart-container
|
||||
.chart
|
||||
= area_chart @dossiers_funnel, ytitle: 'Nb dossiers', label: 'Nb dossiers'
|
||||
= area_chart @dossiers_funnel, ytitle: t('.dossiers_count'), label: t('.dossiers_count')
|
||||
|
||||
.stat-cards
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title TAUX D’ACCEPTATION
|
||||
.stat-card-details depuis le lancement de la procédure
|
||||
%span.stat-card-title= t('.acceptance_rate')
|
||||
.stat-card-details= t('.acceptance_rate_details')
|
||||
.chart-container
|
||||
.chart
|
||||
= pie_chart @termines_states,
|
||||
code: true,
|
||||
colors: %w(#387EC3 #AE2C2B #FAD859),
|
||||
label: 'Taux',
|
||||
label: t('.rate'),
|
||||
suffix: '%',
|
||||
library: { plotOptions: { pie: { dataLabels: { enabled: true, format: '{point.name} : {point.percentage: .1f}%' } } } }
|
||||
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title RÉPARTITION PAR SEMAINE
|
||||
.stat-card-details au cours des 6 derniers mois
|
||||
%span.stat-card-title= t('.weekly_distribution')
|
||||
.stat-card-details= t('.weekly_distribution_details')
|
||||
.chart-container
|
||||
.chart
|
||||
= line_chart @termines_by_week, colors: ["#387EC3", "#AE2C2B", "#FAD859"], ytitle: 'Nb dossiers'
|
||||
= line_chart @termines_by_week, colors: ["#387EC3", "#AE2C2B", "#FAD859"], ytitle: t('.dossiers_count')
|
||||
|
|
|
@ -31,6 +31,26 @@
|
|||
"confidence": "Weak",
|
||||
"note": "explicitely sanitized even if we are using html_safe"
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "4254ed68100af9b496883716b1fd658e1943b2385a0d08de5a6ef5c600c1a8f9",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/traitement.rb",
|
||||
"line": 51,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "ActiveRecord::Base.connection.execute(\"select date_trunc('month', r1.processed_at::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL) as month, count(r1.processed_at)\\nfrom (#{Traitement.select(\"max(traitements.processed_at) as processed_at\").termine.where(:dossier => Dossier.state_termine.where(:groupe_instructeur => groupe_instructeurs)).group(:dossier_id).to_sql}) as r1\\ngroup by date_trunc('month', r1.processed_at::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)\\norder by month desc\\n\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Traitement",
|
||||
"method": "Traitement.count_dossiers_termines_by_month"
|
||||
},
|
||||
"user_input": "Time.zone.formatted_offset",
|
||||
"confidence": "Medium",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Cross-Site Scripting",
|
||||
"warning_code": 2,
|
||||
|
@ -62,26 +82,6 @@
|
|||
"confidence": "Weak",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "6c98e520dd368104bb0c81334875010711cd523afc28057ef86a10930f95c4b7",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/stat.rb",
|
||||
"line": 83,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "association.where(date_attribute => ((3.months.ago.beginning_of_month.to_date..max_date))).group(\"DATE_TRUNC('month', #{date_attribute})\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Stat",
|
||||
"method": "last_four_months_hash"
|
||||
},
|
||||
"user_input": "date_attribute",
|
||||
"confidence": "Weak",
|
||||
"note": "no user input, fixed value"
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
|
@ -102,6 +102,26 @@
|
|||
"confidence": "Medium",
|
||||
"note": "The table and column are escaped, which should make this safe"
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "c0f93612a68c32da58f327e0b5fa33dd42fd8beb2984cf023338c5aadbbdacca",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/stat.rb",
|
||||
"line": 83,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "association.where(date_attribute => ((3.months.ago.beginning_of_month..max_date))).group(\"DATE_TRUNC('month', #{date_attribute}::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Stat",
|
||||
"method": "last_four_months_hash"
|
||||
},
|
||||
"user_input": "date_attribute",
|
||||
"confidence": "Weak",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Redirect",
|
||||
"warning_code": 18,
|
||||
|
@ -109,7 +129,7 @@
|
|||
"check_name": "Redirect",
|
||||
"message": "Possible unprotected redirect",
|
||||
"file": "app/controllers/instructeurs/procedures_controller.rb",
|
||||
"line": 190,
|
||||
"line": 195,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
||||
"code": "redirect_to(Export.find_or_create_export(params[:export_format], (params[:time_span_type] or \"everything\"), current_instructeur.groupe_instructeurs.where(:procedure => procedure)).file.service_url)",
|
||||
"render_path": null,
|
||||
|
@ -125,13 +145,13 @@
|
|||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "dc6d873aff3dc5e51e3349b17e1f35039b23d0bddbf04224b0f1bca3e4608c1e",
|
||||
"fingerprint": "f2bb9bc6a56e44ab36ee18152c657395841cff354baed0a302b8d18650551529",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/stat.rb",
|
||||
"line": 97,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "association.where(\"#{date_attribute} < ?\", max_date).group(\"DATE_TRUNC('month', #{date_attribute})\")",
|
||||
"code": "association.where(\"#{date_attribute} < ?\", max_date).group(\"DATE_TRUNC('month', #{date_attribute}::TIMESTAMPTZ AT TIME ZONE '#{Time.zone.formatted_offset}'::INTERVAL)\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
|
@ -140,9 +160,9 @@
|
|||
},
|
||||
"user_input": "date_attribute",
|
||||
"confidence": "Weak",
|
||||
"note": "no user input, fixed value"
|
||||
"note": ""
|
||||
}
|
||||
],
|
||||
"updated": "2021-11-26 13:22:41 +0100",
|
||||
"updated": "2021-12-01 17:39:08 -1000",
|
||||
"brakeman_version": "5.1.1"
|
||||
}
|
||||
|
|
|
@ -104,3 +104,32 @@ en:
|
|||
codeCertificationCNAV: CNAV certification code
|
||||
codeCategorieInscription: registration category code
|
||||
libelleCategorieInscription: registration category label
|
||||
mesri:
|
||||
libelle: Student status
|
||||
scopes:
|
||||
identifiant:
|
||||
libelle: Identifier
|
||||
ine: INE
|
||||
identite:
|
||||
libelle: Identity
|
||||
nom: last name
|
||||
prenom: first name
|
||||
dateNaissance: date of birth
|
||||
inscriptions:
|
||||
libelle: Registrations
|
||||
statut: status
|
||||
regime: system
|
||||
dateDebutInscription: start date of registration
|
||||
dateFinInscription: end date of registration
|
||||
codeCommune: postal code
|
||||
admissions:
|
||||
libelle: Admissions
|
||||
statut: status
|
||||
regime: system
|
||||
dateDebutAdmission: start date of admission
|
||||
dateFinAdmission: end date of admission
|
||||
codeCommune: postal code
|
||||
etablissements:
|
||||
libelle: Institutions
|
||||
uai: UAI
|
||||
nom: name
|
||||
|
|
|
@ -104,3 +104,32 @@ fr:
|
|||
codeCertificationCNAV: code de certification CNAV
|
||||
codeCategorieInscription: code de catégorie d’inscription
|
||||
libelleCategorieInscription: libellé de catégorie d’inscription
|
||||
mesri:
|
||||
libelle: Statut étudiant
|
||||
scopes:
|
||||
identifiant:
|
||||
libelle: Identifiant
|
||||
ine: INE
|
||||
identite:
|
||||
libelle: Identité
|
||||
nom: nom
|
||||
prenom: prénom
|
||||
dateNaissance: date de naissance
|
||||
inscriptions:
|
||||
libelle: Inscriptions
|
||||
statut: statut
|
||||
regime: régime
|
||||
dateDebutInscription: date de début d'inscription
|
||||
dateFinInscription: date de fin d'inscription
|
||||
codeCommune: code de la commune
|
||||
admissions:
|
||||
libelle: Admissions
|
||||
statut: statut
|
||||
regime: régime
|
||||
dateDebutAdmission: date de début d'admission
|
||||
dateFinAdmission: date de fin d'admission
|
||||
codeCommune: code de la commune
|
||||
etablissements:
|
||||
libelle: Établissements
|
||||
uai: UAI
|
||||
nom: nom
|
||||
|
|
|
@ -418,3 +418,20 @@ en:
|
|||
invalid_password: "The password is not correct."
|
||||
connection_done: "The accounts for FranceConnect and %{application_name} are now merged."
|
||||
merger_token_expired: "Le delay to merge your FranceConnect and %{application_name} accounts is expired. Please retry."
|
||||
shared:
|
||||
procedures:
|
||||
stats:
|
||||
usual_processing_time: "Usual processing time"
|
||||
processing_time_description: "%{percentile}% of submitted files in the last %{span} days were processed in less than %{days} days."
|
||||
processing_time: "Processing time"
|
||||
since_procedure_creation: "since the procedure was created"
|
||||
nb_days: "Nb Days"
|
||||
processing_time_graph_description: "Processing time between instruction and final answer (accepted, rejected or closed) for 90% of files."
|
||||
status_evolution_details: "since the procedure launch"
|
||||
status_evolution: "Evolution of file statuses"
|
||||
acceptance_rate: "Acceptance rate"
|
||||
acceptance_rate_details: "since the procedure launch"
|
||||
rate: "Rate"
|
||||
dossiers_count: "Nb files"
|
||||
weekly_distribution: "Weekly distribution"
|
||||
weekly_distribution_details: "in the last 6 months"
|
||||
|
|
|
@ -24,6 +24,7 @@ fr:
|
|||
utils:
|
||||
'yes': Oui
|
||||
'no': Non
|
||||
i_dont_know: Je ne sais pas
|
||||
deconnexion: "Déconnexion"
|
||||
pj: "Pièces jointes"
|
||||
asterisk_html: Les champs suivis d’un astérisque ( <span class = mandatory> * </span> ) sont obligatoires.
|
||||
|
@ -420,7 +421,7 @@ fr:
|
|||
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 d'accéder aux données familiales (CAF), aux données fiscales (DGFiP) et au statut pôle-emploi d'un citoyen.<br />Renseignez ici le <a href=\"https://api.gouv.fr/les-api/api-particulier/demande-acces\">jeton API Particulier</a> propre à votre démarche."
|
||||
api_particulier_description_html: "%{app_name} utilise <a href=\"https://api.gouv.fr/les-api/api-particulier\">API Particulier</a> qui permet d'accéder aux données familiales (CAF), aux données fiscales (DGFiP), au statut pôle-emploi et au statut étudiant d'un citoyen.<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"
|
||||
|
@ -434,7 +435,7 @@ fr:
|
|||
show:
|
||||
title: "Définir les sources de données"
|
||||
data_sources: "Sources de données"
|
||||
explication_html: "<p>API Particulier facilite l’accès des administrations aux données familiales (CAF), aux données fiscales (DGFiP) et au statut pôle-emploi d'un citoyen pour simplifier les démarches administratives mises en œuvre par les collectivités et les administrations.<br> Cela permet aux administrations d’accéder à des informations certifiées à la source et ainsi : </p> <ul> <li>de s’affranchir des pièces justificatives lors des démarches en ligne,</li> <li>de réduire le nombre d’erreurs de saisie,</li> <li>d’écarter le risque de fraude documentaire.</li> </ul> <p> <strong>Important :</strong> les disposition de l'article <a href='https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000031366350&idArticle=LEGIARTI000031367412&dateTexte=&categorieLien=cid'>L144-8</a> n’autorisent 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 d’un point de vue légal.</p>"
|
||||
explication_html: "<p>API Particulier facilite l’accès des administrations aux données familiales (CAF), aux données fiscales (DGFiP), au statut pôle-emploi et au statut étudiant d'un citoyen pour simplifier les démarches administratives mises en œuvre par les collectivités et les administrations.<br> Cela permet aux administrations d’accéder à des informations certifiées à la source et ainsi : </p> <ul> <li>de s’affranchir des pièces justificatives lors des démarches en ligne,</li> <li>de réduire le nombre d’erreurs de saisie,</li> <li>d’écarter le risque de fraude documentaire.</li> </ul> <p> <strong>Important :</strong> les disposition de l'article <a href='https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000031366350&idArticle=LEGIARTI000031367412&dateTexte=&categorieLien=cid'>L144-8</a> n’autorisent 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 d’un point de vue légal.</p>"
|
||||
update:
|
||||
sources_ok: 'Mise à jour effectuée'
|
||||
procedures:
|
||||
|
@ -442,6 +443,8 @@ fr:
|
|||
ready: "Validé"
|
||||
needs_configuration: "À configurer"
|
||||
configure_api_particulier_token: "Configurer le jeton API particulier"
|
||||
zones:
|
||||
ministeres: Ministères
|
||||
france_connect:
|
||||
particulier:
|
||||
password_confirmation:
|
||||
|
@ -462,3 +465,20 @@ fr:
|
|||
invalid_password: "Mauvais mot de passe"
|
||||
connection_done: "Les comptes FranceConnect et %{application_name} sont à présent fusionnés"
|
||||
merger_token_expired: "Le délai pour fusionner les comptes FranceConnect et %{application_name} est expirée. Veuillez recommencer la procédure pour vous fusionner les comptes."
|
||||
shared:
|
||||
procedures:
|
||||
stats:
|
||||
usual_processing_time: "Temps de traitement usuel"
|
||||
processing_time_description: "%{percentile}% des demandes des %{span} derniers jours ont été traitées en moins de %{days} jours."
|
||||
processing_time: "Temps de traitement"
|
||||
since_procedure_creation: "depuis le lancement de la démarche"
|
||||
nb_days: "Nb Jours"
|
||||
processing_time_graph_description: "Temps de traitement entre le passage en instruction et la réponse (accepté, refusé, ou classé sans suite) pour 90% des dossiers"
|
||||
status_evolution_details: "depuis le lancement de la démarche"
|
||||
status_evolution: "Avancée des dossiers"
|
||||
acceptance_rate: "Taux d’acceptation"
|
||||
acceptance_rate_details: "depuis le lancement de la démarche"
|
||||
rate: "Taux"
|
||||
dossiers_count: "Nb dossiers"
|
||||
weekly_distribution: "Répartition par semaine"
|
||||
weekly_distribution_details: "au cours des 6 derniers mois"
|
||||
|
|
|
@ -39,3 +39,4 @@ fr:
|
|||
cnaf: 'Données de la Caisse nationale des allocations familiales'
|
||||
dgfip: 'Données de la Direction générale des Finances publiques'
|
||||
pole_emploi: 'Situation Pôle emploi'
|
||||
mesri: "Données du Ministère de l'Enseignement Supérieur, de la Recherche et de l'Innovation"
|
||||
|
|
|
@ -15,6 +15,9 @@ en:
|
|||
pole_emploi:
|
||||
identifiant_label: Identifier
|
||||
identifiant_notice: It is usually composed of alphanumeric characters.
|
||||
mesri:
|
||||
ine_label: INE
|
||||
ine_notice: Student National Number. It is usually composed of alphanumeric characters.
|
||||
header:
|
||||
expires_at:
|
||||
brouillon: "Expires at %{date} (%{duree_conservation_totale} months after the creation of this file)"
|
||||
|
@ -42,3 +45,9 @@ en:
|
|||
fetching_data: "Fetching data for identifier %{identifiant}."
|
||||
data_fetched: "Data concerning %{sources} linked to the identifier %{identifiant} has been received from Pôle emploi."
|
||||
data_fetched_title: "Data received from Pôle emploi"
|
||||
mesri:
|
||||
show:
|
||||
not_filled: not filled
|
||||
fetching_data: "Fetching data for INE %{ine}."
|
||||
data_fetched: "Data concerning %{sources} linked to the INE %{ine} has been received from the MESRI."
|
||||
data_fetched_title: "Data received from the MESRI"
|
||||
|
|
|
@ -15,6 +15,9 @@ fr:
|
|||
pole_emploi:
|
||||
identifiant_label: Identifiant
|
||||
identifiant_notice: Il est généralement composé de caractères alphanumériques.
|
||||
mesri:
|
||||
ine_label: INE
|
||||
ine_notice: Identifiant National Étudiant. Il est généralement composé de caractères alphanumériques.
|
||||
header:
|
||||
expires_at:
|
||||
brouillon: "Expirera le %{date} (%{duree_conservation_totale} mois après la création du dossier)"
|
||||
|
@ -44,3 +47,9 @@ fr:
|
|||
fetching_data: "La récupération automatique des données pour l'identifiant %{identifiant} est en cours."
|
||||
data_fetched: "Des données concernant %{sources} liées à l'identifiant %{identifiant} ont été reçues depuis Pôle emploi."
|
||||
data_fetched_title: "Données obtenues de Pôle emploi"
|
||||
mesri:
|
||||
show:
|
||||
not_filled: non renseigné
|
||||
fetching_data: "La récupération automatique des données pour l'INE %{ine} et en cours."
|
||||
data_fetched: "Des données concernant %{sources} liées à l'INE %{ine} ont été reçues depuis le MESRI."
|
||||
data_fetched_title: "Données obtenues du MESRI"
|
||||
|
|
|
@ -102,7 +102,6 @@ Rails.application.routes.draw do
|
|||
get '/users/no_procedure' => 'users/sessions#no_procedure'
|
||||
get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link'
|
||||
get 'lien-envoye' => 'users/sessions#link_sent', as: 'link_sent'
|
||||
get 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent_legacy' # legacy, can be removed as soon as the previous line is deployed to production servers
|
||||
get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent'
|
||||
end
|
||||
|
||||
|
|
|
@ -13,13 +13,13 @@ ministeres:
|
|||
label: "Ministère de l'Économie, des Finances et de la Relance"
|
||||
- MJS:
|
||||
label: "Ministère de la Jeunesse et des Sports"
|
||||
- EN:
|
||||
- MEN:
|
||||
label: "Ministère de l'Éducation nationale, de la Jeunesse et des Sports"
|
||||
- ESR:
|
||||
label: "Ministère de l'Enseignement supérieur, de la Recherche et de l'Innovation"
|
||||
- MI:
|
||||
label: "Ministère de l'Intérieur"
|
||||
- MInArm:
|
||||
- MINARM:
|
||||
label: "Ministère des Armées"
|
||||
- MJ:
|
||||
label: "Ministère de la Justice"
|
||||
|
@ -27,7 +27,7 @@ ministeres:
|
|||
label: "Ministère de la Transition écologique"
|
||||
- MCTRCT:
|
||||
label: "Ministère de la Cohésion des territoires et des Relations avec les collectivités territoriales"
|
||||
- SPM:
|
||||
- PM:
|
||||
label: "Premier ministre"
|
||||
- MER:
|
||||
label: "Ministère de la Mer"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
namespace :after_party do
|
||||
desc 'Deployment task: populate_zones'
|
||||
namespace :zones do
|
||||
task populate_zones: :environment do
|
||||
puts "Running deploy task 'populate_zones'"
|
||||
|
||||
|
@ -9,9 +8,5 @@ namespace :after_party do
|
|||
acronym = ministere.keys.first
|
||||
Zone.create!(acronym: acronym, label: ministere["label"])
|
||||
end
|
||||
# Update task as completed. If you remove the line below, the task will
|
||||
# run with every deploy (or every time you call after_party:run).
|
||||
AfterParty::TaskRecord
|
||||
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
|
||||
end
|
||||
end
|
23
lib/tasks/update_zone_to_procedures.rake
Normal file
23
lib/tasks/update_zone_to_procedures.rake
Normal file
|
@ -0,0 +1,23 @@
|
|||
require Rails.root.join("lib", "tasks", "task_helper")
|
||||
|
||||
namespace :zones do
|
||||
desc <<~EOD
|
||||
Update zone to all procedures
|
||||
rails zones:update_zone_to_procedures\[csv_path\]
|
||||
EOD
|
||||
task :update_zone_to_procedures, [:csv] => :environment do |_t, args|
|
||||
csv = args[:csv]
|
||||
lines = CSV.readlines(csv, headers: true)
|
||||
|
||||
rake_puts "Mise à jour des procédures en cours..."
|
||||
|
||||
errors =
|
||||
UpdateZoneToProceduresService.call(lines)
|
||||
|
||||
if errors.present?
|
||||
errors.each { |error| rake_puts error }
|
||||
end
|
||||
|
||||
rake_puts "Mise à jour terminée"
|
||||
end
|
||||
end
|
|
@ -79,7 +79,13 @@ describe Administrateurs::JetonParticulierController, type: :controller do
|
|||
'pole_emploi_identite',
|
||||
'pole_emploi_adresse',
|
||||
'pole_emploi_contact',
|
||||
'pole_emploi_inscription'
|
||||
'pole_emploi_inscription',
|
||||
'mesri_identifiant',
|
||||
'mesri_identite',
|
||||
'mesri_inscription_etudiant',
|
||||
'mesri_inscription_autre',
|
||||
'mesri_admission',
|
||||
'mesri_etablissements'
|
||||
)
|
||||
expect(procedure.api_particulier_sources).to be_empty
|
||||
end
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
describe StatsController, type: :controller do
|
||||
before { Timecop.travel(Date.parse("2021/12/15")) }
|
||||
after { Timecop.return }
|
||||
|
||||
describe "#last_four_months_hash" do
|
||||
context "while a regular user is logged in" do
|
||||
before do
|
||||
|
@ -14,13 +17,15 @@ describe StatsController, type: :controller do
|
|||
|
||||
let(:association) { Procedure.all }
|
||||
|
||||
subject { @controller.send(:last_four_months_hash, association, :updated_at) }
|
||||
subject { @controller.send(:last_four_months_serie, association, :updated_at) }
|
||||
|
||||
it do
|
||||
expect(subject).to match_array([
|
||||
[I18n.l(62.days.ago.beginning_of_month, format: "%B %Y"), 2],
|
||||
[I18n.l(31.days.ago.beginning_of_month, format: "%B %Y"), 1]
|
||||
])
|
||||
expect(subject).to eq({
|
||||
4.months.ago => 0,
|
||||
3.months.ago => 0,
|
||||
62.days.ago => 2,
|
||||
31.days.ago => 1
|
||||
}.transform_keys { |date| I18n.l(date, format: '%B %Y') })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -38,13 +43,15 @@ describe StatsController, type: :controller do
|
|||
|
||||
let (:association) { Procedure.all }
|
||||
|
||||
subject { @controller.send(:last_four_months_hash, association, :updated_at) }
|
||||
subject { @controller.send(:last_four_months_serie, association, :updated_at) }
|
||||
|
||||
it do
|
||||
expect(subject).to eq([
|
||||
[I18n.l(45.days.ago.beginning_of_month, format: "%B %Y"), 1],
|
||||
[I18n.l(1.day.ago.beginning_of_month, format: "%B %Y"), 2]
|
||||
])
|
||||
expect(subject).to eq({
|
||||
3.months.ago => 0,
|
||||
45.days.ago => 1,
|
||||
1.month.ago => 0,
|
||||
1.day.ago => 2
|
||||
}.transform_keys { |date| I18n.l(date, format: '%B %Y') })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -66,13 +73,13 @@ describe StatsController, type: :controller do
|
|||
context "while a super admin is logged in" do
|
||||
before { allow(@controller).to receive(:super_admin_signed_in?).and_return(true) }
|
||||
|
||||
subject { @controller.send(:cumulative_hash, association, :updated_at) }
|
||||
subject { @controller.send(:cumulative_month_serie, association, :updated_at) }
|
||||
|
||||
it do
|
||||
expect(subject).to eq({
|
||||
Time.utc(2016, 8, 1) => 2,
|
||||
Time.utc(2016, 9, 1) => 4,
|
||||
Time.utc(2016, 10, 1) => 5
|
||||
Date.new(2016, 8, 1) => 2,
|
||||
Date.new(2016, 9, 1) => 4,
|
||||
Date.new(2016, 10, 1) => 5
|
||||
})
|
||||
end
|
||||
end
|
||||
|
@ -80,12 +87,12 @@ describe StatsController, type: :controller do
|
|||
context "while a super admin is not logged in" do
|
||||
before { allow(@controller).to receive(:super_admin_signed_in?).and_return(false) }
|
||||
|
||||
subject { @controller.send(:cumulative_hash, association, :updated_at) }
|
||||
subject { @controller.send(:cumulative_month_serie, association, :updated_at) }
|
||||
|
||||
it do
|
||||
expect(subject).to eq({
|
||||
Time.utc(2016, 8, 1) => 2,
|
||||
Time.utc(2016, 9, 1) => 4
|
||||
Date.new(2016, 8, 1) => 2,
|
||||
Date.new(2016, 9, 1) => 4
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -221,4 +221,22 @@ describe Users::SessionsController, type: :controller do
|
|||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#link_sent' do
|
||||
render_views
|
||||
|
||||
before { get :link_sent, params: { email: link_email } }
|
||||
|
||||
context 'when the email is legit' do
|
||||
let(:link_email) { 'a@a.com' }
|
||||
|
||||
it { expect(response.body).to include(link_email) }
|
||||
end
|
||||
|
||||
context 'when the email is evil' do
|
||||
let(:link_email) { 'Hello, I am an evil email' }
|
||||
|
||||
it { expect(response).to redirect_to(root_path) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
10
spec/factories/bulk_message.rb
Normal file
10
spec/factories/bulk_message.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
FactoryBot.define do
|
||||
factory :bulk_message do
|
||||
body { 'bonjour' }
|
||||
dossier_count { 1 }
|
||||
dossier_state { Dossier.states.fetch(:brouillon) }
|
||||
sent_at { 1.day.ago }
|
||||
instructeur { association :instructeur }
|
||||
groupe_instructeurs { [association(:groupe_instructeur, strategy: :build)] }
|
||||
end
|
||||
end
|
|
@ -197,6 +197,10 @@ FactoryBot.define do
|
|||
type_de_champ { association :type_de_champ_pole_emploi, procedure: dossier.procedure }
|
||||
end
|
||||
|
||||
factory :champ_mesri, class: 'Champs::MesriChamp' do
|
||||
type_de_champ { association :type_de_champ_mesri, procedure: dossier.procedure }
|
||||
end
|
||||
|
||||
factory :champ_siret, class: 'Champs::SiretChamp' do
|
||||
type_de_champ { association :type_de_champ_siret, procedure: dossier.procedure }
|
||||
association :etablissement, factory: [:etablissement]
|
||||
|
|
|
@ -8,5 +8,9 @@ FactoryBot.define do
|
|||
trait :default do
|
||||
label { GroupeInstructeur::DEFAUT_LABEL }
|
||||
end
|
||||
|
||||
trait :with_bulk_message do
|
||||
bulk_messages { [association(:bulk_message, strategy: :build)] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,6 +72,10 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :with_bulk_message do
|
||||
groupe_instructeurs { [association(:groupe_instructeur, :default, :with_bulk_message, procedure: instance, strategy: :build)] }
|
||||
end
|
||||
|
||||
trait :with_logo do
|
||||
logo { Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
|
||||
end
|
||||
|
@ -212,6 +216,12 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :with_mesri do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
build(:type_de_champ_mesri, procedure: procedure)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_explication do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
build(:type_de_champ_explication, procedure: procedure)
|
||||
|
|
|
@ -163,6 +163,9 @@ FactoryBot.define do
|
|||
factory :type_de_champ_pole_emploi do
|
||||
type_champ { TypeDeChamp.type_champs.fetch(:pole_emploi) }
|
||||
end
|
||||
factory :type_de_champ_mesri do
|
||||
type_champ { TypeDeChamp.type_champs.fetch(:mesri) }
|
||||
end
|
||||
factory :type_de_champ_carte do
|
||||
type_champ { TypeDeChamp.type_champs.fetch(:carte) }
|
||||
end
|
||||
|
|
83
spec/fixtures/cassettes/api_particulier/success/etudiants.yml
vendored
Normal file
83
spec/fixtures/cassettes/api_particulier/success/etudiants.yml
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://particulier.api.gouv.fr/api/v2/etudiants?ine=090601811AB
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- demarches-simplifiees.fr
|
||||
Accept:
|
||||
- application/json
|
||||
X-Api-Key:
|
||||
- c6d23f3900b8fb4b3586c4804c051af79062f54b
|
||||
Expect:
|
||||
- ''
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Tue, 16 Mar 2021 17:01:19 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Content-Length:
|
||||
- '805'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Keep-Alive:
|
||||
- timeout=5
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Expose-Headers:
|
||||
- Range,Content-Range,X-Content-Range,X-API-Key
|
||||
Etag:
|
||||
- W/"2eb-A0NiRd+gbJKIAT0y4tR4j9tjXb0"
|
||||
Server:
|
||||
- nginx
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000
|
||||
- max-age=15552000
|
||||
Vary:
|
||||
- Origin, Accept
|
||||
X-Gravitee-Request-Id:
|
||||
- 7bfb7f99-ac2d-4443-bb7f-99ac2d0443c5
|
||||
X-Gravitee-Transaction-Id:
|
||||
- f5dca8b3-2ab7-4c9a-9ca8-b32ab70c9a2b
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{
|
||||
"ine": "090601811AB",
|
||||
"nom": "DUBOIS",
|
||||
"prenom": "Angela Claire Louise",
|
||||
"dateNaissance": "1962-08-24",
|
||||
"inscriptions": [
|
||||
{
|
||||
"statut": "admis",
|
||||
"regime": "formation continue",
|
||||
"dateDebutAdmission": "2021-09-01T00:00:00.000Z",
|
||||
"dateFinAdmission": "2022-08-31T00:00:00.000Z",
|
||||
"etablissement": {
|
||||
"uai": "0751722P",
|
||||
"nom": "Université Pierre et Marie Curie - UPCM (Paris 6)"
|
||||
},
|
||||
"codeCommune": "75106"
|
||||
},
|
||||
{
|
||||
"statut": "inscrit",
|
||||
"regime": "formation continue",
|
||||
"dateDebutInscription": "2022-09-01",
|
||||
"dateFinInscription": "2023-08-31",
|
||||
"etablissement": {
|
||||
"uai": "0751722P",
|
||||
"nom": "Université Pierre et Marie Curie - UPCM (Paris 6)"
|
||||
},
|
||||
"codeCommune": "75106"
|
||||
}
|
||||
]
|
||||
}'
|
||||
recorded_at: Tue, 16 Mar 2021 17:01:18 GMT
|
||||
recorded_with: VCR 6.0.0
|
83
spec/fixtures/cassettes/api_particulier/success/etudiants_invalid.yml
vendored
Normal file
83
spec/fixtures/cassettes/api_particulier/success/etudiants_invalid.yml
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://particulier.api.gouv.fr/api/v2/etudiants?ine=090601811AB
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- demarches-simplifiees.fr
|
||||
Accept:
|
||||
- application/json
|
||||
X-Api-Key:
|
||||
- c6d23f3900b8fb4b3586c4804c051af79062f54b
|
||||
Expect:
|
||||
- ''
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Tue, 16 Mar 2021 17:01:19 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Content-Length:
|
||||
- '806'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Keep-Alive:
|
||||
- timeout=5
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Expose-Headers:
|
||||
- Range,Content-Range,X-Content-Range,X-API-Key
|
||||
Etag:
|
||||
- W/"2eb-A0NiRd+gbJKIAT0y4tR4j9tjXb0"
|
||||
Server:
|
||||
- nginx
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000
|
||||
- max-age=15552000
|
||||
Vary:
|
||||
- Origin, Accept
|
||||
X-Gravitee-Request-Id:
|
||||
- 7bfb7f99-ac2d-4443-bb7f-99ac2d0443c5
|
||||
X-Gravitee-Transaction-Id:
|
||||
- f5dca8b3-2ab7-4c9a-9ca8-b32ab70c9a2b
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{
|
||||
"ine": "090601811AB",
|
||||
"nom": "DUBOIS",
|
||||
"prenom": "Angela Claire Louise",
|
||||
"dateNaissance": "1962-08-24",
|
||||
"inscriptions": [
|
||||
{
|
||||
"statut": "absent",
|
||||
"regime": "formation continue",
|
||||
"dateDebutAdmission": "2021-09-01T00:00:00.000Z",
|
||||
"dateFinAdmission": "2022-08-31T00:00:00.000Z",
|
||||
"etablissement": {
|
||||
"uai": "0751722P",
|
||||
"nom": "Université Pierre et Marie Curie - UPCM (Paris 6)"
|
||||
},
|
||||
"codeCommune": "75106"
|
||||
},
|
||||
{
|
||||
"statut": "inscrit",
|
||||
"regime": "formation continue",
|
||||
"dateDebutInscription": "2022-09-01",
|
||||
"dateFinInscription": "2023-08-31",
|
||||
"etablissement": {
|
||||
"uai": "0751722P",
|
||||
"nom": "Université Pierre et Marie Curie - UPCM (Paris 6)"
|
||||
},
|
||||
"codeCommune": "75106"
|
||||
}
|
||||
]
|
||||
}'
|
||||
recorded_at: Tue, 16 Mar 2021 17:01:18 GMT
|
||||
recorded_with: VCR 6.0.0
|
|
@ -25,7 +25,7 @@ http_interactions:
|
|||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '257'
|
||||
- '1068'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Keep-Alive:
|
||||
|
@ -38,6 +38,6 @@ http_interactions:
|
|||
- max-age=15552000
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"_id":"1d99db5a-a099-4314-ad2f-2707c6b505a6","name":"Application de sandbox","scopes":["cnaf_allocataires","cnaf_enfants","cnaf_adresse","cnaf_quotient_familial","dgfip_declarant1_nom","dgfip_declarant1_nom_naissance","dgfip_declarant1_prenoms","dgfip_declarant1_date_naissance","dgfip_declarant2_nom","dgfip_declarant2_nom_naissance","dgfip_declarant2_prenoms","dgfip_declarant2_date_naissance","dgfip_date_recouvrement","dgfip_date_etablissement","dgfip_adresse_fiscale_taxation","dgfip_adresse_fiscale_annee","dgfip_nombre_parts","dgfip_nombre_personnes_a_charge","dgfip_situation_familiale","dgfip_revenu_brut_global","dgfip_revenu_imposable","dgfip_impot_revenu_net_avant_corrections","dgfip_montant_impot","dgfip_revenu_fiscal_reference","dgfip_annee_impot","dgfip_annee_revenus","dgfip_erreur_correctif","dgfip_situation_partielle", "pole_emploi_identite","pole_emploi_adresse","pole_emploi_contact","pole_emploi_inscription"]}'
|
||||
string: '{"_id":"1d99db5a-a099-4314-ad2f-2707c6b505a6","name":"Application de sandbox","scopes":["cnaf_allocataires","cnaf_enfants","cnaf_adresse","cnaf_quotient_familial","dgfip_declarant1_nom","dgfip_declarant1_nom_naissance","dgfip_declarant1_prenoms","dgfip_declarant1_date_naissance","dgfip_declarant2_nom","dgfip_declarant2_nom_naissance","dgfip_declarant2_prenoms","dgfip_declarant2_date_naissance","dgfip_date_recouvrement","dgfip_date_etablissement","dgfip_adresse_fiscale_taxation","dgfip_adresse_fiscale_annee","dgfip_nombre_parts","dgfip_nombre_personnes_a_charge","dgfip_situation_familiale","dgfip_revenu_brut_global","dgfip_revenu_imposable","dgfip_impot_revenu_net_avant_corrections","dgfip_montant_impot","dgfip_revenu_fiscal_reference","dgfip_annee_impot","dgfip_annee_revenus","dgfip_erreur_correctif","dgfip_situation_partielle", "pole_emploi_identite","pole_emploi_adresse","pole_emploi_contact","pole_emploi_inscription","mesri_identifiant","mesri_identite","mesri_inscription_etudiant","mesri_inscription_autre","mesri_admission","mesri_etablissements"]}'
|
||||
recorded_at: Tue, 16 Mar 2021 15:25:24 GMT
|
||||
recorded_with: VCR 6.0.0
|
||||
|
|
34
spec/fixtures/files/api_particulier/etudiants.json
vendored
Normal file
34
spec/fixtures/files/api_particulier/etudiants.json
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"identifiant": {
|
||||
"ine": "090601811AB"
|
||||
},
|
||||
"identite": {
|
||||
"nom": "DUBOIS",
|
||||
"prenom": "Angela Claire Louise",
|
||||
"dateNaissance": "1962-08-24"
|
||||
},
|
||||
"admissions": [
|
||||
{
|
||||
"statut": "admis",
|
||||
"regime": "formation continue",
|
||||
"dateDebutAdmission": "2021-09-01T00:00:00.000Z",
|
||||
"dateFinAdmission": "2022-08-31T00:00:00.000Z",
|
||||
"codeCommune": "75106"
|
||||
}
|
||||
],
|
||||
"inscriptions": [
|
||||
{
|
||||
"statut": "inscrit",
|
||||
"regime": "formation continue",
|
||||
"dateDebutInscription": "2022-09-01",
|
||||
"dateFinInscription": "2023-08-31",
|
||||
"codeCommune": "75106"
|
||||
}
|
||||
],
|
||||
"etablissements": [
|
||||
{
|
||||
"uai": "0751722P",
|
||||
"nom": "Université Pierre et Marie Curie - UPCM (Paris 6)"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -41,7 +41,13 @@ describe APIParticulier::API do
|
|||
'pole_emploi_identite',
|
||||
'pole_emploi_adresse',
|
||||
'pole_emploi_contact',
|
||||
'pole_emploi_inscription'
|
||||
'pole_emploi_inscription',
|
||||
'mesri_identifiant',
|
||||
'mesri_identite',
|
||||
'mesri_inscription_etudiant',
|
||||
'mesri_inscription_autre',
|
||||
'mesri_admission',
|
||||
'mesri_etablissements'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
70
spec/lib/api_particulier/mesri_adapter_spec.rb
Normal file
70
spec/lib/api_particulier/mesri_adapter_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
describe APIParticulier::MesriAdapter do
|
||||
let(:adapter) { described_class.new(api_particulier_token, ine, requested_sources) }
|
||||
|
||||
before { stub_const('API_PARTICULIER_URL', 'https://particulier.api.gouv.fr/api') }
|
||||
|
||||
describe '#to_params' do
|
||||
let(:api_particulier_token) { 'c6d23f3900b8fb4b3586c4804c051af79062f54b' }
|
||||
let(:ine) { '090601811AB' }
|
||||
|
||||
subject { VCR.use_cassette(cassette) { adapter.to_params } }
|
||||
|
||||
context 'when the api answer is valid' do
|
||||
let(:cassette) { 'api_particulier/success/etudiants' }
|
||||
|
||||
context 'when the token has all the MESRI scopes' do
|
||||
context 'and all the sources are requested' do
|
||||
let(:requested_sources) do
|
||||
{
|
||||
'mesri' => {
|
||||
'identifiant' => ['ine'],
|
||||
'identite' => ['nom', 'prenom', 'dateNaissance'],
|
||||
'inscriptions' => ['statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune'],
|
||||
'admissions' => ['statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune'],
|
||||
'etablissements' => ['uai', 'nom']
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:result) { JSON.parse(File.read('spec/fixtures/files/api_particulier/etudiants.json')) }
|
||||
|
||||
it { is_expected.to eq(result) }
|
||||
end
|
||||
|
||||
context 'when no sources is requested' do
|
||||
let(:requested_sources) { {} }
|
||||
|
||||
it { is_expected.to eq({}) }
|
||||
end
|
||||
|
||||
context 'when an admission statut is requested' do
|
||||
let(:requested_sources) { { 'mesri' => { 'admissions' => ['statut'] } } }
|
||||
|
||||
it { is_expected.to eq('admissions' => [{ 'statut' => 'admis' }]) }
|
||||
end
|
||||
|
||||
context 'when an inscription statut is requested' do
|
||||
let(:requested_sources) { { 'mesri' => { 'inscriptions' => ['statut'] } } }
|
||||
|
||||
it { is_expected.to eq('inscriptions' => [{ 'statut' => 'inscrit' }]) }
|
||||
end
|
||||
|
||||
context 'when a first name is requested' do
|
||||
let(:requested_sources) { { 'mesri' => { 'identite' => ['prenom'] } } }
|
||||
|
||||
it { is_expected.to eq('identite' => { 'prenom' => 'Angela Claire Louise' }) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the api answer is invalid' do
|
||||
let(:cassette) { 'api_particulier/success/etudiants_invalid' }
|
||||
|
||||
context 'when no sources is requested' do
|
||||
let(:requested_sources) { {} }
|
||||
|
||||
it { expect { subject }.to raise_error(APIParticulier::MesriAdapter::InvalidSchemaError) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,6 +62,21 @@ describe APIParticulier::Services::SourcesService do
|
|||
it { is_expected.to match(pole_emploi_identite_et_adresse) }
|
||||
end
|
||||
|
||||
context 'when a procedure has a mesri_identite and a mesri_etablissements scopes' do
|
||||
let(:api_particulier_scopes) { ['mesri_identite', 'mesri_etablissements'] }
|
||||
|
||||
let(:mesri_identite_and_etablissements) do
|
||||
{
|
||||
'mesri' => {
|
||||
'identite' => ['nom', 'prenom', 'dateNaissance'],
|
||||
'etablissements' => ['uai', 'nom']
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to match(mesri_identite_and_etablissements) }
|
||||
end
|
||||
|
||||
context 'when a procedure has an unknown scope' do
|
||||
let(:api_particulier_scopes) { ['unknown_scope'] }
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
describe '20211116140232_populate_zones' do
|
||||
let(:rake_task) { Rake::Task['after_party:populate_zones'] }
|
||||
subject(:run_task) do
|
||||
rake_task.invoke
|
||||
end
|
||||
|
||||
it 'populates zones' do
|
||||
run_task
|
||||
expect(Zone.find_by(acronym: 'SPM').label).to eq "Premier ministre"
|
||||
end
|
||||
end
|
11
spec/lib/tasks/populate_zones_spec.rb
Normal file
11
spec/lib/tasks/populate_zones_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
describe 'populate_zones' do
|
||||
let(:rake_task) { Rake::Task['zones:populate_zones'] }
|
||||
subject(:run_task) do
|
||||
rake_task.invoke
|
||||
end
|
||||
|
||||
it 'populates zones' do
|
||||
run_task
|
||||
expect(Zone.find_by(acronym: 'PM').label).to eq "Premier ministre"
|
||||
end
|
||||
end
|
36
spec/lib/utils/retryable_spec.rb
Normal file
36
spec/lib/utils/retryable_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
describe Utils::Retryable do
|
||||
Includer = Struct.new(:something) do
|
||||
include Utils::Retryable
|
||||
|
||||
def caller(max_attempt:, errors:)
|
||||
with_retry(max_attempt: max_attempt, errors: errors) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subject { Includer.new("test") }
|
||||
let(:spy) { double() }
|
||||
|
||||
describe '#with_retry' do
|
||||
it 'works while retry count is less than max attempts' do
|
||||
divider_that_raise_error = 0
|
||||
divider_that_works = 1
|
||||
expect(spy).to receive(:divider).and_return(divider_that_raise_error, divider_that_works)
|
||||
result = subject.caller(max_attempt: 2, errors: [ZeroDivisionError]) { 10 / spy.divider }
|
||||
expect(result).to eq(10 / divider_that_works)
|
||||
end
|
||||
|
||||
it 're raise error if it occures more than max_attempt' do
|
||||
expect(spy).to receive(:divider).and_return(0, 0)
|
||||
expect { subject.caller(max_attempt: 1, errors: [ZeroDivisionError]) { 0 / spy.divider } }
|
||||
.to raise_error(ZeroDivisionError)
|
||||
end
|
||||
|
||||
it 'does not retry other errors' do
|
||||
expect(spy).to receive(:divider).and_raise(StandardError).once
|
||||
expect { subject.caller(max_attempt: 2, errors: [ZeroDivisionError]) { 0 / spy.divider } }
|
||||
.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
56
spec/models/champs/mesri_champ_spec.rb
Normal file
56
spec/models/champs/mesri_champ_spec.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
describe Champs::MesriChamp, type: :model do
|
||||
let(:champ) { described_class.new }
|
||||
|
||||
describe 'INE' do
|
||||
before do
|
||||
champ.ine = '090601811AB'
|
||||
end
|
||||
|
||||
it 'saves INE' do
|
||||
expect(champ.ine).to eq('090601811AB')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'external_id' do
|
||||
context 'when no data is given' do
|
||||
before do
|
||||
champ.ine = ''
|
||||
champ.save
|
||||
end
|
||||
|
||||
it { expect(champ.external_id).to be_nil }
|
||||
end
|
||||
|
||||
context 'when all data required for an external fetch are given' do
|
||||
before do
|
||||
champ.ine = '090601811AB'
|
||||
champ.save
|
||||
end
|
||||
|
||||
it { expect(JSON.parse(champ.external_id)).to eq("ine" => "090601811AB") }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate' do
|
||||
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_mesri)) }
|
||||
let(:validation_context) { :create }
|
||||
|
||||
subject { champ.valid?(validation_context) }
|
||||
|
||||
before do
|
||||
champ.ine = ine
|
||||
end
|
||||
|
||||
context 'when INE is valid' do
|
||||
let(:ine) { '090601811AB' }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when INE is nil' do
|
||||
let(:ine) { nil }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -262,6 +262,16 @@ describe Procedure do
|
|||
let(:procedure) { build(:procedure, monavis_embed: monavis_bleu) }
|
||||
it { expect(procedure.valid?).to eq(true) }
|
||||
end
|
||||
|
||||
context 'Monavis embed code with voxusages is allowed' do
|
||||
monavis_issue_phillipe = <<-MSG
|
||||
<a href="https://voxusagers.numerique.gouv.fr/Demarches/3193?&view-mode=formulaire-avis&nd_mode=en-ligne-enti%C3%A8rement&nd_source=button&key=58e099a09c02abe629c14905ed2b055d">
|
||||
<img src="https://monavis.numerique.gouv.fr/monavis-static/bouton-bleu.png" alt="Je donne mon avis" title="Je donne mon avis sur cette démarche" />
|
||||
</a>
|
||||
MSG
|
||||
let(:procedure) { build(:procedure, monavis_embed: monavis_issue_phillipe) }
|
||||
it { expect(procedure.valid?).to eq(true) }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'duree de conservation' do
|
||||
|
@ -1115,7 +1125,7 @@ describe Procedure do
|
|||
end
|
||||
|
||||
describe "#destroy" do
|
||||
let(:procedure) { create(:procedure, :closed, :with_type_de_champ) }
|
||||
let(:procedure) { create(:procedure, :closed, :with_type_de_champ, :with_bulk_message) }
|
||||
|
||||
before do
|
||||
procedure.discard!
|
||||
|
|
|
@ -57,24 +57,24 @@ describe Stat do
|
|||
create(:dossier, state: :en_construction, depose_at: i.months.ago)
|
||||
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
|
||||
end
|
||||
rs = Stat.send(:cumulative_hash, [
|
||||
rs = Stat.send(:cumulative_month_serie, [
|
||||
[Dossier.state_not_brouillon, :depose_at],
|
||||
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
||||
])
|
||||
expect(rs).to eq({
|
||||
12.months.ago.utc.beginning_of_month => 2,
|
||||
11.months.ago.utc.beginning_of_month => 4,
|
||||
10.months.ago.utc.beginning_of_month => 6,
|
||||
9.months.ago.utc.beginning_of_month => 8,
|
||||
8.months.ago.utc.beginning_of_month => 10,
|
||||
7.months.ago.utc.beginning_of_month => 12,
|
||||
6.months.ago.utc.beginning_of_month => 14,
|
||||
5.months.ago.utc.beginning_of_month => 16,
|
||||
4.months.ago.utc.beginning_of_month => 18,
|
||||
3.months.ago.utc.beginning_of_month => 20,
|
||||
2.months.ago.utc.beginning_of_month => 22,
|
||||
1.month.ago.utc.beginning_of_month => 24
|
||||
})
|
||||
12 => 2,
|
||||
11 => 4,
|
||||
10 => 6,
|
||||
9 => 8,
|
||||
8 => 10,
|
||||
7 => 12,
|
||||
6 => 14,
|
||||
5 => 16,
|
||||
4 => 18,
|
||||
3 => 20,
|
||||
2 => 22,
|
||||
1 => 24
|
||||
}.transform_keys { |i| i.months.ago.beginning_of_month.to_date })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -85,15 +85,16 @@ describe Stat do
|
|||
create(:dossier, state: :en_construction, depose_at: i.months.ago)
|
||||
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
|
||||
end
|
||||
rs = Stat.send(:last_four_months_hash, [
|
||||
rs = Stat.send(:last_four_months_serie, [
|
||||
[Dossier.state_not_brouillon, :depose_at],
|
||||
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
||||
])
|
||||
expect(rs).to eq([
|
||||
["août 2021", 2],
|
||||
["septembre 2021", 2],
|
||||
["octobre 2021", 2]
|
||||
])
|
||||
expect(rs).to eq({
|
||||
"juillet 2021" => 2,
|
||||
"août 2021" => 2,
|
||||
"septembre 2021" => 2,
|
||||
"octobre 2021" => 2
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ describe ProcedureArchiveService do
|
|||
let(:year) { 2020 }
|
||||
let(:month) { 3 }
|
||||
let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") }
|
||||
|
||||
describe '#create_pending_archive' do
|
||||
context 'for a specific month' do
|
||||
it 'creates a pending archive' do
|
||||
|
@ -27,7 +28,7 @@ describe ProcedureArchiveService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#collect_files_archive' do
|
||||
describe '#old_collect_files_archive' do
|
||||
before do
|
||||
create_dossier_for_month(year, month)
|
||||
create_dossier_for_month(2020, month)
|
||||
|
@ -116,6 +117,123 @@ describe ProcedureArchiveService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#new_collect_files_archive' do
|
||||
before { Flipper.enable_actor(:zip_using_binary, procedure) }
|
||||
let!(:dossier) { create_dossier_for_month(year, month) }
|
||||
let!(:dossier_2020) { create_dossier_for_month(2020, month) }
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
context 'for a specific month' do
|
||||
let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month) }
|
||||
let(:year) { 2021 }
|
||||
let(:mailer) { double('mailer', deliver_later: true) }
|
||||
|
||||
it 'collect files' do
|
||||
expect(InstructeurMailer).to receive(:send_archive).and_return(mailer)
|
||||
service.collect_files_archive(archive, instructeur)
|
||||
|
||||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
|
||||
structure = [
|
||||
"procedure-#{procedure.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2021-00-00-#{dossier.attestation.pdf.id % 10000}.pdf",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id}.pdf"
|
||||
]
|
||||
expect(files.map(&:filename)).to match_array(structure)
|
||||
end
|
||||
expect(archive.file.attached?).to be_truthy
|
||||
end
|
||||
|
||||
context 'with a missing file' do
|
||||
let(:pj) do
|
||||
PiecesJustificativesService::FakeAttachment.new(
|
||||
file: StringIO.new('coucou'),
|
||||
filename: "export-dossier.pdf",
|
||||
name: 'pdf_export_for_instructeur',
|
||||
id: 1,
|
||||
created_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
|
||||
let(:bad_pj) do
|
||||
PiecesJustificativesService::FakeAttachment.new(
|
||||
file: nil,
|
||||
filename: "cni.png",
|
||||
name: 'cni.png',
|
||||
id: 2,
|
||||
created_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
|
||||
let(:documents) { [pj, bad_pj] }
|
||||
before do
|
||||
allow(PiecesJustificativesService).to receive(:liste_documents).and_return(documents)
|
||||
end
|
||||
|
||||
it 'collect files without raising exception' do
|
||||
expect { service.collect_files_archive(archive, instructeur) }.not_to raise_exception
|
||||
end
|
||||
|
||||
it 'add bug report to archive' do
|
||||
service.collect_files_archive(archive, instructeur)
|
||||
|
||||
archive.file.open do |f|
|
||||
zip_entries = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
structure = [
|
||||
"procedure-#{procedure.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/export-dossier-05-03-2020-00-00-1.pdf",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2021-00-00-#{dossier.id}.pdf",
|
||||
"procedure-#{procedure.id}/LISEZMOI.txt"
|
||||
]
|
||||
expect(zip_entries.map(&:filename)).to match_array(structure)
|
||||
zip_entries.map do |entry|
|
||||
next unless entry.filename == "procedure-#{procedure.id}/LISEZMOI.txt"
|
||||
extracted_content = ""
|
||||
extractor = entry.extractor_from(f)
|
||||
extracted_content << extractor.extract(1024 * 1024) until extractor.eof?
|
||||
expect(extracted_content).to match(/Impossible de .* .*cni.*png/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for all months' do
|
||||
let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending') }
|
||||
let(:mailer) { double('mailer', deliver_later: true) }
|
||||
|
||||
it 'collect files' do
|
||||
expect(InstructeurMailer).to receive(:send_archive).and_return(mailer)
|
||||
|
||||
service.collect_files_archive(archive, instructeur)
|
||||
|
||||
archive = Archive.last
|
||||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
structure = [
|
||||
"procedure-#{procedure.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/pieces_justificatives/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier.attestation.pdf.id % 10000}.pdf",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier.id}/export-#{dossier.id}-05-03-2020-00-00-#{dossier.id}.pdf",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier_2020.id}/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier_2020.id}/export-#{dossier_2020.id}-05-03-2020-00-00-#{dossier_2020.id}.pdf",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier_2020.id}/pieces_justificatives/",
|
||||
"procedure-#{procedure.id}/dossier-#{dossier_2020.id}/pieces_justificatives/attestation-dossier--05-03-2020-00-00-#{dossier_2020.attestation.pdf.id % 10000}.pdf"
|
||||
]
|
||||
expect(files.map(&:filename)).to match_array(structure)
|
||||
end
|
||||
expect(archive.file.attached?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_dossier_for_month(year, month)
|
||||
|
|
|
@ -80,6 +80,7 @@ describe ProcedureExportService do
|
|||
"cnaf",
|
||||
"dgfip",
|
||||
"pole_emploi",
|
||||
"mesri",
|
||||
"text"
|
||||
]
|
||||
end
|
||||
|
@ -170,6 +171,7 @@ describe ProcedureExportService do
|
|||
"cnaf",
|
||||
"dgfip",
|
||||
"pole_emploi",
|
||||
"mesri",
|
||||
"text"
|
||||
]
|
||||
end
|
||||
|
@ -256,6 +258,7 @@ describe ProcedureExportService do
|
|||
"cnaf",
|
||||
"dgfip",
|
||||
"pole_emploi",
|
||||
"mesri",
|
||||
"text"
|
||||
]
|
||||
end
|
||||
|
|
57
spec/services/update_zone_to_procedures_service_spec.rb
Normal file
57
spec/services/update_zone_to_procedures_service_spec.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
describe UpdateZoneToProceduresService do
|
||||
before(:all) do
|
||||
Rake::Task['zones:populate_zones'].invoke
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
Zone.destroy_all
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
let(:procedure1) { create(:procedure, zone: nil) }
|
||||
let(:procedure2) { create(:procedure, zone: nil) }
|
||||
|
||||
subject { described_class.call(lines) }
|
||||
|
||||
context 'nominal case' do
|
||||
let(:lines) do
|
||||
[
|
||||
{ "id" => procedure1.id, "POL_PUB_MINISTERE RATTACHEMENT" => "PM" },
|
||||
{ "id" => procedure2.id, "POL_PUB_MINISTERE RATTACHEMENT" => "MI" }
|
||||
]
|
||||
end
|
||||
|
||||
it 'updates zone to procedures' do
|
||||
errors = subject
|
||||
|
||||
expect(errors).to eq []
|
||||
expect(procedure1.reload.zone.acronym).to eq("PM")
|
||||
expect(procedure2.reload.zone.acronym).to eq("MI")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown procedure' do
|
||||
let(:lines) do
|
||||
[
|
||||
{ "id" => procedure1.id + procedure2.id, "POL_PUB_MINISTERE RATTACHEMENT" => "PM" }
|
||||
]
|
||||
end
|
||||
it 'returns errors' do
|
||||
errors = subject
|
||||
expect(errors).to eq ["Procedure #{procedure1.id + procedure2.id} introuvable"]
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown zone' do
|
||||
let(:lines) do
|
||||
[
|
||||
{ "id" => procedure1.id, "POL_PUB_MINISTERE RATTACHEMENT" => "YOUPI" }
|
||||
]
|
||||
end
|
||||
it 'returns errors' do
|
||||
errors = subject
|
||||
expect(errors).to eq ["Zone YOUPI introuvable"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,6 +26,13 @@ describe 'fetch API Particulier Data', js: true do
|
|||
'adresse' => ['INSEECommune', 'codePostal', 'localite', 'ligneVoie', 'ligneComplementDestinataire', 'ligneComplementAdresse', 'ligneComplementDistribution', 'ligneNom'],
|
||||
'contact' => ['email', 'telephone', 'telephone2'],
|
||||
'inscription' => ['dateInscription', 'dateCessationInscription', 'codeCertificationCNAV', 'codeCategorieInscription', 'libelleCategorieInscription']
|
||||
},
|
||||
'mesri' => {
|
||||
'identifiant' => ['ine'],
|
||||
'identite' => ['nom', 'prenom', 'dateNaissance'],
|
||||
'inscriptions' => ['statut', 'regime', 'dateDebutInscription', 'dateFinInscription', 'codeCommune'],
|
||||
'admissions' => ['statut', 'regime', 'dateDebutAdmission', 'dateFinAdmission', 'codeCommune'],
|
||||
'etablissements' => ['uai', 'nom']
|
||||
}
|
||||
}
|
||||
end
|
||||
|
@ -158,6 +165,37 @@ describe 'fetch API Particulier Data', js: true do
|
|||
check("libellé de catégorie d’inscription")
|
||||
end
|
||||
|
||||
within('#mesri-identifiant') do
|
||||
check('INE')
|
||||
end
|
||||
|
||||
within('#mesri-identite') do
|
||||
check('nom')
|
||||
check('prénom')
|
||||
check('date de naissance')
|
||||
end
|
||||
|
||||
within('#mesri-inscriptions') do
|
||||
check('statut')
|
||||
check('régime')
|
||||
check("date de début d'inscription")
|
||||
check("date de fin d'inscription")
|
||||
check("code de la commune")
|
||||
end
|
||||
|
||||
within('#mesri-admissions') do
|
||||
check('statut')
|
||||
check('régime')
|
||||
check("date de début d'admission")
|
||||
check("date de fin d'admission")
|
||||
check("code de la commune")
|
||||
end
|
||||
|
||||
within('#mesri-etablissements') do
|
||||
check('UAI')
|
||||
check('nom')
|
||||
end
|
||||
|
||||
click_on 'Enregistrer'
|
||||
|
||||
within('#cnaf-enfants') do
|
||||
|
@ -166,10 +204,11 @@ describe 'fetch API Particulier Data', js: true do
|
|||
|
||||
procedure.reload
|
||||
|
||||
expect(procedure.api_particulier_sources.keys).to contain_exactly('cnaf', 'dgfip', 'pole_emploi')
|
||||
expect(procedure.api_particulier_sources.keys).to contain_exactly('cnaf', 'dgfip', 'pole_emploi', 'mesri')
|
||||
expect(procedure.api_particulier_sources['cnaf'].keys).to contain_exactly('adresse', 'allocataires', 'enfants', 'quotient_familial')
|
||||
expect(procedure.api_particulier_sources['dgfip'].keys).to contain_exactly('declarant1', 'declarant2', 'echeance_avis', 'foyer_fiscal', 'agregats_fiscaux', 'complements')
|
||||
expect(procedure.api_particulier_sources['pole_emploi'].keys).to contain_exactly('identite', 'adresse', 'contact', 'inscription')
|
||||
expect(procedure.api_particulier_sources['mesri'].keys).to contain_exactly('identifiant', 'identite', 'inscriptions', 'admissions', 'etablissements')
|
||||
|
||||
procedure.api_particulier_sources.each do |provider, scopes|
|
||||
scopes.each do |scope, fields|
|
||||
|
@ -203,10 +242,11 @@ describe 'fetch API Particulier Data', js: true do
|
|||
let(:reference_avis) { '2097699999077' }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:identifiant) { 'georges_moustaki_77' }
|
||||
let(:ine) { '090601811AB' }
|
||||
let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' }
|
||||
|
||||
let(:procedure) do
|
||||
create(:procedure, :for_individual, :with_service, :with_cnaf, :with_dgfip, :with_pole_emploi, :published,
|
||||
create(:procedure, :for_individual, :with_service, :with_cnaf, :with_dgfip, :with_pole_emploi, :with_mesri, :published,
|
||||
libelle: 'libellé de la procédure',
|
||||
path: 'libelle-de-la-procedure',
|
||||
instructeurs: [instructeur],
|
||||
|
@ -336,6 +376,67 @@ describe 'fetch API Particulier Data', js: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'MESRI' do
|
||||
let(:api_particulier_token) { 'c6d23f3900b8fb4b3586c4804c051af79062f54b' }
|
||||
|
||||
scenario 'it can fill a MESRI field' do
|
||||
visit commencer_path(path: procedure.path)
|
||||
click_on 'Commencer la démarche'
|
||||
|
||||
choose 'Madame'
|
||||
fill_in 'individual_nom', with: 'Dubois'
|
||||
fill_in 'individual_prenom', with: 'Angela Claire Louise'
|
||||
|
||||
click_button('Continuer')
|
||||
|
||||
fill_in "INE", with: 'wrong code'
|
||||
|
||||
blur
|
||||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
|
||||
dossier = Dossier.last
|
||||
mesri_champ = dossier.champs.fourth
|
||||
|
||||
expect(mesri_champ.ine).to eq('wrong code')
|
||||
|
||||
fill_in "INE", with: ine
|
||||
|
||||
VCR.use_cassette('api_particulier/success/etudiants') do
|
||||
perform_enqueued_jobs { click_on 'Déposer le dossier' }
|
||||
end
|
||||
|
||||
visit demande_dossier_path(dossier)
|
||||
expect(page).to have_content(/Des données.*ont été reçues depuis le MESRI/)
|
||||
|
||||
log_out
|
||||
|
||||
login_as instructeur.user, scope: :user
|
||||
|
||||
visit instructeur_dossier_path(procedure, dossier)
|
||||
|
||||
expect(page).to have_content('INE 090601811AB')
|
||||
|
||||
expect(page).to have_content('nom DUBOIS')
|
||||
expect(page).to have_content('prénom Angela Claire Louise')
|
||||
expect(page).to have_content('date de naissance 24 août 1962')
|
||||
|
||||
expect(page).to have_content('statut inscrit')
|
||||
expect(page).to have_content('régime formation continue')
|
||||
expect(page).to have_content("date de début d'inscription 1 septembre 2022")
|
||||
expect(page).to have_content("date de fin d'inscription 31 août 2023")
|
||||
expect(page).to have_content('code de la commune 75106')
|
||||
|
||||
expect(page).to have_content('statut admis')
|
||||
expect(page).to have_content('régime formation continue')
|
||||
expect(page).to have_content("date de début d'admission 1 septembre 2021")
|
||||
expect(page).to have_content("date de fin d'admission 31 août 2022")
|
||||
expect(page).to have_content('code de la commune 75106')
|
||||
|
||||
expect(page).to have_content('UAI 0751722P')
|
||||
expect(page).to have_content('nom Université Pierre et Marie Curie - UPCM (Paris 6)')
|
||||
end
|
||||
end
|
||||
|
||||
scenario 'it can fill a DGFiP field' do
|
||||
visit commencer_path(path: procedure.path)
|
||||
click_on 'Commencer la démarche'
|
||||
|
|
|
@ -8,9 +8,9 @@ describe 'users/statistiques/show.html.haml', type: :view do
|
|||
subject { render }
|
||||
|
||||
it "display stats" do
|
||||
expect(subject).to have_text("RÉPARTITION PAR SEMAINE")
|
||||
expect(subject).to have_text("AVANCÉE DES DOSSIERS")
|
||||
expect(subject).to have_text("TAUX D’ACCEPTATION")
|
||||
expect(subject).to have_text("Répartition par semaine")
|
||||
expect(subject).to have_text("Avancée des dossiers")
|
||||
expect(subject).to have_text("Taux d’acceptation")
|
||||
expect(subject).to have_text(procedure.libelle)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue