diff --git a/Gemfile b/Gemfile
index 425f8926766..19a36e41d23 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,6 +32,7 @@ gem 'devise-i18n'
gem 'devise-two-factor'
gem 'discard'
gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails
+gem 'dry-monads'
gem 'elastic-apm'
gem 'flipper'
gem 'flipper-active_record'
diff --git a/Gemfile.lock b/Gemfile.lock
index bb9cc73e70c..1b6f1176a26 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -235,7 +235,14 @@ GEM
dotenv (= 2.7.6)
railties (>= 3.2)
dry-cli (1.0.0)
+ dry-core (1.0.0)
+ concurrent-ruby (~> 1.0)
+ zeitwerk (~> 2.6)
dry-inflector (0.2.0)
+ dry-monads (1.6.0)
+ concurrent-ruby (~> 1.0)
+ dry-core (~> 1.0, < 2)
+ zeitwerk (~> 2.6)
dumb_delegator (1.0.0)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
@@ -852,6 +859,7 @@ DEPENDENCIES
devise-two-factor
discard
dotenv-rails
+ dry-monads
elastic-apm
factory_bot
flipper
diff --git a/app/assets/stylesheets/admin-procedures-list.scss b/app/assets/stylesheets/admin-procedures-list.scss
deleted file mode 100644
index 8369465a0a7..00000000000
--- a/app/assets/stylesheets/admin-procedures-list.scss
+++ /dev/null
@@ -1,17 +0,0 @@
-// Push the timestamps column to the right of the row
-@import "colors";
-
-.admin-procedures-list-timestamps {
- margin-left: auto;
-}
-
-// Fix a Safari flexbox bug where the inner procedure logo
-// would stretch the container vertically.
-// See https://stackoverflow.com/questions/57516373/image-stretching-in-flexbox-in-safari
-.admin-procedures-list-row.infos {
- align-items: flex-start;
-
- a:not(:hover) {
- background-image: none; // remove DSFR underline
- }
-}
diff --git a/app/assets/stylesheets/buttons.scss b/app/assets/stylesheets/buttons.scss
index ae961f1f2c6..bbcab2c2a64 100644
--- a/app/assets/stylesheets/buttons.scss
+++ b/app/assets/stylesheets/buttons.scss
@@ -194,7 +194,7 @@
text-align: left;
top: 5 * $default-spacer;
cursor: default;
- z-index: 10;
+ z-index: 11;
list-style: none;
a {
diff --git a/app/assets/stylesheets/card.scss b/app/assets/stylesheets/card.scss
index 2db51bef097..8f8bd538ba6 100644
--- a/app/assets/stylesheets/card.scss
+++ b/app/assets/stylesheets/card.scss
@@ -4,20 +4,26 @@
.card {
padding: ($default-spacer * 3) ($default-spacer * 2);
border: 1px solid $border-grey;
- margin-bottom: $default-spacer * 2;
+ margin-bottom: $default-spacer * 4;
background: #FFFFFF;
- .notice {
- font-size: 16px;
- color: #666666;
- margin-top: -8px;
- margin-bottom: 16px;
- }
-
.card-title {
+ color: $black;
font-weight: bold;
font-size: 20px;
+ line-height: 1.5rem;
margin-bottom: $default-spacer * 2;
+
+ a:not(:hover) {
+ background-image: none; // remove DSFR underline
+ }
+ }
+
+ .logo {
+ width: auto;
+ max-width: 50px;
+ height: fit-content;
+ margin-right: $default-spacer * 2;
}
&.feedback {
diff --git a/app/assets/stylesheets/flex.scss b/app/assets/stylesheets/flex.scss
index e8c78a5a9f0..bc0b81f4813 100644
--- a/app/assets/stylesheets/flex.scss
+++ b/app/assets/stylesheets/flex.scss
@@ -11,6 +11,10 @@
align-items: flex-start;
}
+ &.align-end {
+ align-items: end;
+ }
+
&.align-baseline {
align-items: baseline;
}
diff --git a/app/components/dossiers/champs_rows_show_component/champs_rows_show_component.html.haml b/app/components/dossiers/champs_rows_show_component/champs_rows_show_component.html.haml
index 1ae64cfc8c7..3e9523527b9 100644
--- a/app/components/dossiers/champs_rows_show_component/champs_rows_show_component.html.haml
+++ b/app/components/dossiers/champs_rows_show_component/champs_rows_show_component.html.haml
@@ -48,6 +48,8 @@
= render partial: "shared/champs/rna/show", locals: { champ: champ, profile: @profile }
- when TypeDeChamp.type_champs.fetch(:epci)
= render partial: "shared/champs/epci/show", locals: { champ: champ }
+ - when TypeDeChamp.type_champs.fetch(:cojo)
+ = render partial: "shared/champs/cojo/show", locals: { champ: champ, profile: @profile }
- when TypeDeChamp.type_champs.fetch(:date)
%p= champ.to_s
- when TypeDeChamp.type_champs.fetch(:datetime)
diff --git a/app/components/dsfr/alert_component.rb b/app/components/dsfr/alert_component.rb
index d1fe2856620..7323f639b91 100644
--- a/app/components/dsfr/alert_component.rb
+++ b/app/components/dsfr/alert_component.rb
@@ -13,20 +13,25 @@ def prefix_for_state
end
def alert_class(state)
- ["fr-alert fr-alert--#{state}", extra_class_names].compact.flatten
+ class_names(
+ "fr-alert fr-alert--#{state}" => true,
+ "fr-alert--sm" => size == :sm,
+ extra_class_names => true
+ )
end
private
- def initialize(state:, title:, extra_class_names: nil, heading_level: 'h3')
+ def initialize(state:, title: '', size: '', extra_class_names: nil, heading_level: 'h3')
@state = state
@title = title
+ @size = size
@block = block
@extra_class_names = extra_class_names
@heading_level = heading_level
end
- attr_reader :state, :title, :block, :extra_class_names, :heading_level
+ attr_reader :state, :title, :size, :block, :extra_class_names, :heading_level
private
end
diff --git a/app/components/dsfr/alert_component/alert_component.html.haml b/app/components/dsfr/alert_component/alert_component.html.haml
index b8e848bdbf6..744d56529b9 100644
--- a/app/components/dsfr/alert_component/alert_component.html.haml
+++ b/app/components/dsfr/alert_component/alert_component.html.haml
@@ -1,4 +1,5 @@
%div{ class: alert_class(state) }
- = content_tag(heading_level, class: 'fr-alert__title') do
- = "#{prefix_for_state}#{title}"
+ - if size != :sm
+ = content_tag(heading_level, class: 'fr-alert__title') do
+ = "#{prefix_for_state}#{title}"
= body
diff --git a/app/components/editable_champ/cojo_component.rb b/app/components/editable_champ/cojo_component.rb
new file mode 100644
index 00000000000..e49c44b3d44
--- /dev/null
+++ b/app/components/editable_champ/cojo_component.rb
@@ -0,0 +1,9 @@
+class EditableChamp::COJOComponent < EditableChamp::EditableChampBaseComponent
+ def input_group_class
+ if @champ.accreditation_success?
+ 'fr-input-group--valid'
+ elsif @champ.accreditation_error?
+ 'fr-input-group--error'
+ end
+ end
+end
diff --git a/app/components/editable_champ/cojo_component/cojo_component.en.yml b/app/components/editable_champ/cojo_component/cojo_component.en.yml
new file mode 100644
index 00000000000..88069f92788
--- /dev/null
+++ b/app/components/editable_champ/cojo_component/cojo_component.en.yml
@@ -0,0 +1,7 @@
+---
+en:
+ accreditation_number_label: Accreditation number
+ accreditation_number_notice: Identification number issued by Paris 2024
+ accreditation_birthdate_label: Date of birth
+ accreditation_number_error: Invalid accreditation number
+ accreditation_number_verification_pending: Accreditation number verification in progress
diff --git a/app/components/editable_champ/cojo_component/cojo_component.fr.yml b/app/components/editable_champ/cojo_component/cojo_component.fr.yml
new file mode 100644
index 00000000000..914b945957a
--- /dev/null
+++ b/app/components/editable_champ/cojo_component/cojo_component.fr.yml
@@ -0,0 +1,7 @@
+---
+fr:
+ accreditation_number_label: Numéro d‘accréditation
+ accreditation_number_notice: Numéro d‘identification délivré par Paris 2024
+ accreditation_birthdate_label: Date de naissance
+ accreditation_number_error: Le numéro d‘accréditation est incorrect
+ accreditation_number_verification_pending: Vérification du numéro d‘accréditation en cours
diff --git a/app/components/editable_champ/cojo_component/cojo_component.html.haml b/app/components/editable_champ/cojo_component/cojo_component.html.haml
new file mode 100644
index 00000000000..80ac7ccf35a
--- /dev/null
+++ b/app/components/editable_champ/cojo_component/cojo_component.html.haml
@@ -0,0 +1,23 @@
+
+.fr-input-group{ class: input_group_class }
+ = @form.label :accreditation_number, for: @champ.accreditation_number_input_id, class: 'fr-label' do
+ - safe_join [t('.accreditation_number_label'), @champ.required? ? render(EditableChamp::AsteriskMandatoryComponent.new) : ''], ' '
+ %p.fr-hint-text{ id: dom_id(@champ, :accreditation_number_notice) }= t('.accreditation_number_notice')
+ = @form.text_field :accreditation_number,
+ required: @champ.required?,
+ aria: { describedby: [dom_id(@champ, :accreditation_number_notice), @champ.accreditation_error? ? dom_id(@champ, :accreditation_number_error) : nil].compact.join(' ') },
+ data: { controller: 'format', format: 'integer' },
+ class: "width-33-desktop fr-input small-margin", id: @champ.accreditation_number_input_id
+
+ - if @champ.accreditation_error?
+ %p.fr-error-text{ id: dom_id(@champ, :accreditation_number_error) }= t('.accreditation_number_error')
+ - elsif @champ.fetch_external_data_pending?
+ %p.fr-info-text= t('.accreditation_number_verification_pending')
+
+.fr-input-group{ class: input_group_class }
+ = @form.label :accreditation_birthdate, for: @champ.accreditation_birthdate_input_id, class: 'fr-label' do
+ - safe_join [t('.accreditation_birthdate_label'), @champ.required? ? render(EditableChamp::AsteriskMandatoryComponent.new) : ''], ' '
+ = @form.date_field :accreditation_birthdate,
+ required: @champ.required?,
+ aria: { describedby: dom_id(@champ, :accreditation_birthdate) },
+ class: "width-33-desktop fr-input small-margin", id: @champ.accreditation_birthdate_input_id
diff --git a/app/components/editable_champ/editable_champ_component.rb b/app/components/editable_champ/editable_champ_component.rb
index f2051df8a7f..f335a181e88 100644
--- a/app/components/editable_champ/editable_champ_component.rb
+++ b/app/components/editable_champ/editable_champ_component.rb
@@ -25,15 +25,35 @@ def html_options
"hidden": !@champ.visible?
),
id: @champ.input_group_id,
- data: { controller: stimulus_controller, **data_dependent_conditions }
+ data: { controller: stimulus_controller, **data_dependent_conditions, **stimulus_values }
}
end
+ def stimulus_values
+ if @champ.fetch_external_data_pending?
+ { turbo_poll_url_value: }
+ else
+ {}
+ end
+ end
+
+ def turbo_poll_url_value
+ if @champ.private?
+ annotation_instructeur_dossier_path(@champ.dossier.procedure, @champ.dossier, @champ)
+ else
+ champ_dossier_path(@champ.dossier, @champ)
+ end
+ end
+
def stimulus_controller
if autosave_enabled?
# This is an editable champ. Lets find what controllers it might need.
controllers = ['autosave']
+ if @champ.fetch_external_data_pending?
+ controllers << 'turbo-poll'
+ end
+
controllers.join(' ')
end
end
diff --git a/app/components/types_de_champ_editor/champ_component.rb b/app/components/types_de_champ_editor/champ_component.rb
index 2ee7c9e6ae4..9841c1ce0b6 100644
--- a/app/components/types_de_champ_editor/champ_component.rb
+++ b/app/components/types_de_champ_editor/champ_component.rb
@@ -93,8 +93,8 @@ def filter_block_type_champ(type_champ)
end
def filter_featured_type_champ(type_champ)
- feature_name = TypeDeChamp::FEATURE_FLAGS[type_champ]
- feature_name.blank? || feature_enabled?(feature_name)
+ feature_name = TypeDeChamp::FEATURE_FLAGS[type_champ.to_sym]
+ feature_name.blank? || feature_enabled?(feature_name) || procedure.feature_enabled?(feature_name)
end
def filter_type_champ(type_champ)
diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb
index 2e5b23d7892..f746db660b1 100644
--- a/app/controllers/instructeurs/dossiers_controller.rb
+++ b/app/controllers/instructeurs/dossiers_controller.rb
@@ -305,6 +305,20 @@ def print
render layout: "print"
end
+ def annotation
+ @dossier = dossier_with_champs(pj_template: false)
+ annotation = @dossier.champs_private_all.find(params[:annotation_id])
+
+ respond_to do |format|
+ format.turbo_stream do
+ @to_show, @to_hide = []
+ @to_update = [annotation]
+
+ render :update_annotations
+ end
+ end
+ end
+
def telecharger_pjs
files = ActiveStorage::DownloadableFile.create_list_from_dossiers(Dossier.where(id: dossier.id), with_champs_private: true, include_infos_administration: true)
cleaned_files = ActiveStorage::DownloadableFile.cleanup_list_from_dossier(files)
diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb
index 38e5d8e1f49..621c89fbec2 100644
--- a/app/controllers/users/dossiers_controller.rb
+++ b/app/controllers/users/dossiers_controller.rb
@@ -9,11 +9,11 @@ class DossiersController < UserController
INSTANCE_ACIONS_ALLOWED_TO_OWNER_OR_INVITE = []
ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all] + INSTANCE_ACTIONS_ALLOWED_TO_ANY_USER
- ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update, :create_commentaire, :papertrail, :restore] + INSTANCE_ACIONS_ALLOWED_TO_OWNER_OR_INVITE
+ ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update, :create_commentaire, :papertrail, :restore, :champ] + INSTANCE_ACIONS_ALLOWED_TO_OWNER_OR_INVITE
before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE
before_action :ensure_ownership_or_invitation!, only: ACTIONS_ALLOWED_TO_OWNER_OR_INVITE
- before_action :ensure_dossier_can_be_updated, only: [:update_identite, :update_siret, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update]
+ before_action :ensure_dossier_can_be_updated, only: [:update_identite, :update_siret, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update, :champ]
before_action :ensure_dossier_can_be_filled, only: [:brouillon, :modifier, :submit_brouillon, :submit_en_construction, :update]
before_action :ensure_dossier_can_be_viewed, only: [:show]
before_action :forbid_invite_submission!, only: [:submit_brouillon]
@@ -31,9 +31,9 @@ def index
@dossiers_invites = current_user.dossiers_invites.merge(dossiers_visibles)
@dossiers_supprimes_recemment = current_user.dossiers.hidden_by_user.merge(dossiers)
@dossiers_supprimes_definitivement = current_user.deleted_dossiers.includes(:procedure).order_by_updated_at
- @dossier_transfers = DossierTransfer.for_email(current_user.email)
+ @dossier_transferes = dossiers_visibles.where(dossier_transfer_id: DossierTransfer.for_email(current_user.email).ids)
@dossiers_close_to_expiration = current_user.dossiers.close_to_expiration.merge(dossiers_visibles)
- @statut = statut(@user_dossiers, @dossiers_traites, @dossiers_invites, @dossiers_supprimes_recemment, @dossiers_supprimes_definitivement, @dossier_transfers, @dossiers_close_to_expiration, params[:statut])
+ @statut = statut(@user_dossiers, @dossiers_traites, @dossiers_invites, @dossiers_supprimes_recemment, @dossiers_supprimes_definitivement, @dossier_transferes, @dossiers_close_to_expiration, params[:statut])
@dossiers = case @statut
when 'en-cours'
@@ -47,7 +47,7 @@ def index
when 'dossiers-supprimes-definitivement'
@dossiers_supprimes_definitivement
when 'dossiers-transferes'
- @dossier_transfers
+ @dossier_transferes
when 'dossiers-expirant'
@dossiers_close_to_expiration
end.page(page)
@@ -293,6 +293,20 @@ def merci
@dossier = current_user.dossiers.includes(:procedure).find(params[:id])
end
+ def champ
+ @dossier = dossier_with_champs(pj_template: false)
+ champ = @dossier.champs_public_all.find(params[:champ_id])
+
+ respond_to do |format|
+ format.turbo_stream do
+ @to_show, @to_hide = []
+ @to_update = [champ]
+
+ render :update, layout: false
+ end
+ end
+ end
+
def create_commentaire
@commentaire = CommentaireService.create(current_user, dossier, commentaire_params)
@@ -411,14 +425,14 @@ def clone
# if the status tab is filled, then this tab
# else first filled tab
# else en-cours
- def statut(mes_dossiers, dossiers_traites, dossiers_invites, dossiers_supprimes_recemment, dossiers_supprimes_definitivement, dossier_transfers, dossiers_close_to_expiration, params_statut)
+ def statut(mes_dossiers, dossiers_traites, dossiers_invites, dossiers_supprimes_recemment, dossiers_supprimes_definitivement, dossier_transferes, dossiers_close_to_expiration, params_statut)
tabs = {
'en-cours' => mes_dossiers.present?,
'traites' => dossiers_traites.present?,
'dossiers-invites' => dossiers_invites.present?,
'dossiers-supprimes-recemment' => dossiers_supprimes_recemment.present?,
'dossiers-supprimes-definitivement' => dossiers_supprimes_definitivement.present?,
- 'dossiers-transferes' => dossier_transfers.present?,
+ 'dossiers-transferes' => dossier_transferes.present?,
'dossiers-expirant' => dossiers_close_to_expiration.present?
}
if tabs[params_statut]
@@ -476,17 +490,30 @@ def page
def champs_public_params
champs_params = params.require(:dossier).permit(champs_public_attributes: [
- :id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :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, :code_departement, value: []
- ] + TypeDeChamp::INSTANCE_CHAMPS_PARAMS
+ :id,
+ :value,
+ :value_other,
+ :external_id,
+ :primary_value,
+ :secondary_value,
+ :numero_allocataire,
+ :code_postal,
+ :identifiant,
+ :numero_fiscal,
+ :reference_avis,
+ :ine,
+ :piece_justificative_file,
+ :code_departement,
+ :accreditation_number,
+ :accreditation_birthdate,
+ value: []
] + TypeDeChamp::INSTANCE_CHAMPS_PARAMS)
champs_params[:champs_public_all_attributes] = champs_params.delete(:champs_public_attributes) || {}
champs_params
end
def dossier_scope
- if action_name == 'update'
+ if action_name == 'update' || action_name == 'champ'
Dossier.visible_by_user.or(Dossier.for_procedure_preview).or(Dossier.for_editing_fork)
elsif action_name == 'restore'
Dossier.hidden_by_user
diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb
index f7dd1503fbd..6ca0e765a80 100644
--- a/app/graphql/api/v2/schema.rb
+++ b/app/graphql/api/v2/schema.rb
@@ -87,6 +87,7 @@ def self.resolve_type(type_definition, object, ctx)
Types::Champs::Descriptor::CiviliteChampDescriptorType,
Types::Champs::Descriptor::CnafChampDescriptorType,
Types::Champs::Descriptor::CodePostalDePolynesieChampDescriptorType,
+ Types::Champs::Descriptor::COJOChampDescriptorType,
Types::Champs::Descriptor::CommuneChampDescriptorType,
Types::Champs::Descriptor::CommuneDePolynesieChampDescriptorType,
Types::Champs::Descriptor::DateChampDescriptorType,
diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql
index 4aa4a13a155..09b934b766b 100644
--- a/app/graphql/schema.graphql
+++ b/app/graphql/schema.graphql
@@ -190,6 +190,34 @@ exceed the size of a 32-bit integer, it's encoded as a string.
"""
scalar BigInt
+type COJOChampDescriptor implements ChampDescriptor {
+ """
+ Description des champs d’un bloc répétable.
+ """
+ champDescriptors: [ChampDescriptor!] @deprecated(reason: "Utilisez le champ `RepetitionChampDescriptor.champ_descriptors` à la place.")
+
+ """
+ Description du champ.
+ """
+ description: String
+ id: ID!
+
+ """
+ Libellé du champ.
+ """
+ label: String!
+
+ """
+ Est-ce que le champ est obligatoire ?
+ """
+ required: Boolean!
+
+ """
+ Type de la valeur du champ.
+ """
+ type: TypeDeChamp! @deprecated(reason: "Utilisez le champ `__typename` à la place.")
+}
+
type CarteChamp implements Champ {
geoAreas: [GeoArea!]!
id: ID!
@@ -3988,6 +4016,11 @@ enum TypeDeChamp {
"""
code_postal_de_polynesie
+ """
+ Accréditation Paris 2024
+ """
+ cojo
+
"""
Commune de Polynésie
"""
diff --git a/app/graphql/schema.json b/app/graphql/schema.json
index 9d3fa48c469..6bb3353a80f 100644
--- a/app/graphql/schema.json
+++ b/app/graphql/schema.json
@@ -952,6 +952,131 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "COJOChampDescriptor",
+ "description": null,
+ "fields": [
+ {
+ "name": "champDescriptors",
+ "description": "Description des champs d’un bloc répétable.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INTERFACE",
+ "name": "ChampDescriptor",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Utilisez le champ `RepetitionChampDescriptor.champ_descriptors` à la place."
+ },
+ {
+ "name": "description",
+ "description": "Description du champ.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "label",
+ "description": "Libellé du champ.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "required",
+ "description": "Est-ce que le champ est obligatoire ?",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": "Type de la valeur du champ.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TypeDeChamp",
+ "ofType": null
+ }
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Utilisez le champ `__typename` à la place."
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "ChampDescriptor",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "CarteChamp",
@@ -1479,6 +1604,11 @@
"name": "AnnuaireEducationChampDescriptor",
"ofType": null
},
+ {
+ "kind": "OBJECT",
+ "name": "COJOChampDescriptor",
+ "ofType": null
+ },
{
"kind": "OBJECT",
"name": "CarteChampDescriptor",
@@ -18047,6 +18177,12 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "cojo",
+ "description": "Accréditation Paris 2024",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "nationalites",
"description": "Nationalités",
diff --git a/app/graphql/types/champ_descriptor_type.rb b/app/graphql/types/champ_descriptor_type.rb
index 029fbc0e287..c48c17f8f3b 100644
--- a/app/graphql/types/champ_descriptor_type.rb
+++ b/app/graphql/types/champ_descriptor_type.rb
@@ -109,6 +109,8 @@ def resolve_type(object, context)
Types::Champs::Descriptor::MesriChampDescriptorType
when TypeDeChamp.type_champs.fetch(:epci)
Types::Champs::Descriptor::EpciChampDescriptorType
+ when TypeDeChamp.type_champs.fetch(:cojo)
+ Types::Champs::Descriptor::COJOChampDescriptorType
end
end
end
diff --git a/app/graphql/types/champs/descriptor/cojo_champ_descriptor_type.rb b/app/graphql/types/champs/descriptor/cojo_champ_descriptor_type.rb
new file mode 100644
index 00000000000..4843cc12d73
--- /dev/null
+++ b/app/graphql/types/champs/descriptor/cojo_champ_descriptor_type.rb
@@ -0,0 +1,5 @@
+module Types::Champs::Descriptor
+ class COJOChampDescriptorType < Types::BaseObject
+ implements Types::ChampDescriptorType
+ end
+end
diff --git a/app/javascript/controllers/format_controller.ts b/app/javascript/controllers/format_controller.ts
index 855324860d1..a6a9ad14cda 100644
--- a/app/javascript/controllers/format_controller.ts
+++ b/app/javascript/controllers/format_controller.ts
@@ -15,6 +15,12 @@ export class FormatController extends ApplicationController {
const target = event.target as HTMLInputElement;
target.value = this.formatIBAN(target.value);
});
+ break;
+ case 'integer':
+ this.on('input', (event) => {
+ const target = event.target as HTMLInputElement;
+ target.value = this.formatInteger(target.value);
+ });
}
}
@@ -28,4 +34,8 @@ export class FormatController extends ApplicationController {
.replace(/(.{4})/g, '$1 ')
.trim();
}
+
+ private formatInteger(value: string) {
+ return value.replace(/[^\d]/g, '');
+ }
}
diff --git a/app/jobs/champ_fetch_external_data_job.rb b/app/jobs/champ_fetch_external_data_job.rb
index 790052863ba..4e11bd96a01 100644
--- a/app/jobs/champ_fetch_external_data_job.rb
+++ b/app/jobs/champ_fetch_external_data_job.rb
@@ -1,9 +1,28 @@
class ChampFetchExternalDataJob < ApplicationJob
+ include Dry::Monads[:result]
+
def perform(champ, external_id)
return if champ.external_id != external_id
return if champ.data.present?
- return if (data = champ.fetch_external_data).blank?
- champ.update_with_external_data!(data: data)
+ Sentry.set_tags(champ: champ.id)
+ Sentry.set_extras(external_id:)
+
+ result = champ.fetch_external_data
+
+ if result.is_a?(Dry::Monads::Result)
+ case result
+ in Success(data)
+ champ.update_with_external_data!(data:)
+ in Failure(retryable: true, reason:)
+ champ.log_fetch_external_data_exception(reason)
+ throw reason
+ in Failure(retryable: false, reason:)
+ champ.log_fetch_external_data_exception(reason)
+ Sentry.capture_exception(reason)
+ end
+ elsif result.present?
+ champ.update_with_external_data!(data: result)
+ end
end
end
diff --git a/app/jobs/pjs_migration_job.rb b/app/jobs/pjs_migration_job.rb
index d964b3f95ee..4a44b59e4a1 100644
--- a/app/jobs/pjs_migration_job.rb
+++ b/app/jobs/pjs_migration_job.rb
@@ -10,7 +10,7 @@ def perform(blob_id)
client = service.client
container = service.container
old_key = blob.key
- new_key = "#{blob.created_at.year}/#{old_key[0..1]}/#{old_key[2..3]}/#{old_key}"
+ new_key = "#{blob.created_at.strftime('%Y/%m/%d')}/#{old_key[0..1]}/#{old_key}"
excon_response = client.copy_object(container,
old_key,
diff --git a/app/lib/api/client.rb b/app/lib/api/client.rb
new file mode 100644
index 00000000000..31507f72fc7
--- /dev/null
+++ b/app/lib/api/client.rb
@@ -0,0 +1,97 @@
+class API::Client
+ include Dry::Monads[:result]
+
+ TIMEOUT = 10
+
+ def call(url:, params: nil, body: nil, json: nil, headers: nil, method: :get, authorization_token: nil, schema: nil, timeout: TIMEOUT)
+ response = case method
+ when :get
+ Typhoeus.get(url,
+ headers: headers_with_authorization(headers, authorization_token),
+ params:,
+ timeout: TIMEOUT)
+ when :post
+ Typhoeus.post(url,
+ headers: headers_with_authorization(headers, json, authorization_token),
+ body: json.nil? ? body : json.to_json,
+ timeout: TIMEOUT)
+ end
+ handle_response(response, schema:)
+ end
+
+ private
+
+ def headers_with_authorization(headers, json, authorization_token)
+ headers = headers || {}
+ headers['authorization'] = "Bearer #{authorization_token}" if authorization_token.present?
+ headers['content-type'] = 'application/json' if json.present?
+ headers
+ end
+
+ OK = Data.define(:body, :response)
+ Error = Data.define(:type, :code, :retryable, :reason)
+
+ def handle_response(response, schema:)
+ if response.success?
+ scope = Sentry.get_current_scope
+ if scope.extra.key?(:external_id)
+ scope.set_extras(raw_body: response.body.to_s)
+ end
+ body = parse_body(response.body)
+ case body
+ in Success(body)
+ if !schema || schema.valid?(body)
+ Success(OK[body.deep_symbolize_keys, response])
+ else
+ Failure(Error[:schema, response.code, false, SchemaError.new(schema.validate(body))])
+ end
+ in Failure(reason)
+ Failure(Error[:json, response.code, false, reason])
+ end
+ elsif response.timed_out?
+ Failure(Error[:timeout, response.code, true, HTTPError.new(response)])
+ elsif response.code != 0
+ Failure(Error[:http, response.code, true, HTTPError.new(response)])
+ else
+ Failure(Error[:network, response.code, true, HTTPError.new(response)])
+ end
+ end
+
+ def parse_body(body)
+ Success(JSON.parse(body))
+ rescue JSON::ParserError => error
+ Failure(error)
+ end
+
+ class SchemaError < StandardError
+ attr_reader :errors
+
+ def initialize(errors)
+ @errors = errors.to_a
+
+ super(@errors.map(&:to_json).join("\n"))
+ end
+ end
+
+ class HTTPError < StandardError
+ attr_reader :response
+
+ def initialize(response)
+ @response = response
+
+ uri = URI.parse(response.effective_url)
+
+ msg = <<~TEXT
+ url: #{uri.host}#{uri.path}
+ HTTP error code: #{response.code}
+ body: #{CGI.escape(response.body)}
+ curl message: #{response.return_message}
+ total time: #{response.total_time}
+ connect time: #{response.connect_time}
+ response headers: #{response.headers}
+ TEXT
+
+ super(msg)
+ end
+ end
+end
diff --git a/app/models/champ.rb b/app/models/champ.rb
index 316236adf47..cd758c8a863 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -228,6 +228,16 @@ def fetch_external_data?
false
end
+ def poll_external_data?
+ false
+ end
+
+ def fetch_external_data_pending?
+ # We don't have a good mechanism right now to know if the last fetch has errored. So, in order
+ # to ensure we don't poll to infinity, we stop after 5 minutes no matter what.
+ fetch_external_data? && poll_external_data? && external_id.present? && data.nil? && updated_at > 5.minutes.ago
+ end
+
def fetch_external_data
raise NotImplemented.new(:fetch_external_data)
end
diff --git a/app/models/champs/cojo_champ.rb b/app/models/champs/cojo_champ.rb
new file mode 100644
index 00000000000..b69c1c6b6b7
--- /dev/null
+++ b/app/models/champs/cojo_champ.rb
@@ -0,0 +1,86 @@
+# == Schema Information
+#
+# Table name: champs
+#
+# id :integer not null, primary key
+# data :jsonb
+# fetch_external_data_exceptions :string is an Array
+# prefilled :boolean
+# private :boolean default(FALSE), not null
+# rebased_at :datetime
+# 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
+# row_id :string
+# type_de_champ_id :integer
+#
+class Champs::COJOChamp < Champ
+ store_accessor :value_json, :accreditation_number, :accreditation_birthdate
+ store_accessor :data, :accreditation_success, :accreditation_first_name, :accreditation_last_name
+
+ after_validation :update_external_id
+
+ def accreditation_birthdate
+ Date.parse(super)
+ rescue ArgumentError, TypeError
+ nil
+ end
+
+ def accreditation_success?
+ accreditation_success == true
+ end
+
+ def accreditation_error?
+ accreditation_success == false
+ end
+
+ def blank?
+ accreditation_success.nil?
+ end
+
+ def fetch_external_data?
+ true
+ end
+
+ def poll_external_data?
+ true
+ end
+
+ def fetch_external_data
+ COJOService.new.(accreditation_number:, accreditation_birthdate:)
+ end
+
+ def to_s
+ "#{accreditation_number} – #{accreditation_birthdate}"
+ end
+
+ def accreditation_number_input_id
+ "#{input_id}-accreditation_number"
+ end
+
+ def accreditation_birthdate_input_id
+ "#{input_id}-accreditation_birthdate"
+ end
+
+ def focusable_input_id
+ accreditation_number_input_id
+ end
+
+ private
+
+ def update_external_id
+ if accreditation_number_changed? || accreditation_birthdate_changed?
+ if accreditation_number.present? && accreditation_birthdate.present? && /\A\d+\z/.match?(accreditation_number)
+ self.external_id = { accreditation_number:, accreditation_birthdate: }.to_json
+ else
+ self.external_id = nil
+ end
+ end
+ end
+end
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index d345d5c2756..cadf5278d8c 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -18,7 +18,11 @@ class TypeDeChamp < ApplicationRecord
self.ignored_columns += [:migrated_parent, :revision_id, :parent_id, :order_place]
FILE_MAX_SIZE = 200.megabytes
- FEATURE_FLAGS = { 'visa' => 'visa', 'tefenua' => 'tefenua' }
+ FEATURE_FLAGS = {
+ cojo: :cojo_type_de_champ,
+ visa: :visa,
+ tefenua: :tefenua
+ }
MINIMUM_TEXTAREA_CHARACTER_LIMIT_LENGTH = 400
INSTANCE_TYPE_CHAMPS = {
@@ -86,7 +90,8 @@ class TypeDeChamp < ApplicationRecord
cnaf: REFERENTIEL_EXTERNE,
dgfip: REFERENTIEL_EXTERNE,
pole_emploi: REFERENTIEL_EXTERNE,
- mesri: REFERENTIEL_EXTERNE
+ mesri: REFERENTIEL_EXTERNE,
+ cojo: REFERENTIEL_EXTERNE
}.merge(INSTANCE_TYPE_DE_CHAMP_TO_CATEGORIE)
enum type_champs: {
@@ -125,7 +130,8 @@ class TypeDeChamp < ApplicationRecord
dgfip: 'dgfip',
pole_emploi: 'pole_emploi',
mesri: 'mesri',
- epci: 'epci'
+ epci: 'epci',
+ cojo: 'cojo'
}.merge(INSTANCE_TYPE_CHAMPS)
INSTANCE_OPTIONS = [:parcelles, :batiments, :zones_manuelles, :min, :max, :level, :accredited_users]
@@ -639,7 +645,8 @@ def self.refresh_after_update?(type_champ)
type_champs.fetch(:dossier_link),
type_champs.fetch(:linked_drop_down_list),
type_champs.fetch(:drop_down_list),
- type_champs.fetch(:textarea)
+ type_champs.fetch(:textarea),
+ type_champs.fetch(:cojo)
true
else
false
diff --git a/app/models/types_de_champ/cojo_type_de_champ.rb b/app/models/types_de_champ/cojo_type_de_champ.rb
new file mode 100644
index 00000000000..2747a34c7d2
--- /dev/null
+++ b/app/models/types_de_champ/cojo_type_de_champ.rb
@@ -0,0 +1,2 @@
+class TypesDeChamp::COJOTypeDeChamp < TypesDeChamp::TextTypeDeChamp
+end
diff --git a/app/schemas/accreditation-cojo.json b/app/schemas/accreditation-cojo.json
new file mode 100644
index 00000000000..b6bbeef83b1
--- /dev/null
+++ b/app/schemas/accreditation-cojo.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://demarches-simplifiees.fr/accreditation-cojo.schema.json",
+ "title": "Accreditation COJO",
+ "type": "object",
+ "properties": {
+ "firstName": { "type": "string" },
+ "lastName": { "type": "string" },
+ "individualExistance": {
+ "enum": ["Yes", "No"]
+ }
+ },
+ "required": ["individualExistance"]
+}
diff --git a/app/services/cojo_service.rb b/app/services/cojo_service.rb
new file mode 100644
index 00000000000..22f7c1d7ed1
--- /dev/null
+++ b/app/services/cojo_service.rb
@@ -0,0 +1,56 @@
+class COJOService
+ include Dry::Monads[:result]
+
+ def call(accreditation_number:, accreditation_birthdate:)
+ result = API::Client.new.(url:,
+ json: {
+ accreditationNumber: accreditation_number.to_i,
+ birthdate: accreditation_birthdate&.strftime('%d/%m/%Y')
+ },
+ authorization_token:,
+ schema:,
+ method: :post)
+
+ case result
+ in Success(body:)
+ accreditation_success = body[:individualExistance] == 'Yes'
+ Success({
+ accreditation_success:,
+ accreditation_first_name: accreditation_success ? body[:firstName] : nil,
+ accreditation_last_name: accreditation_success ? body[:lastName] : nil
+ })
+ in Failure(code:, reason:) if code.in?(401..403)
+ Failure(API::Client::Error[:unauthorized, code, false, reason])
+ else
+ result
+ end
+ end
+
+ private
+
+ def schema
+ JSONSchemer.schema(Rails.root.join('app/schemas/accreditation-cojo.json'))
+ end
+
+ def url
+ "#{API_COJO_URL}/api/accreditation"
+ end
+
+ def authorization_token
+ rsa_private_key&.then { JWT.encode(jwt_payload, _1, 'RS256') }
+ end
+
+ def jwt_payload
+ {
+ iss: APPLICATION_NAME,
+ iat: Time.zone.now.to_i,
+ exp: 1.hour.from_now.to_i
+ }
+ end
+
+ def rsa_private_key
+ if ENV['COJO_JWT_RSA_PRIVATE_KEY'].present?
+ OpenSSL::PKey::RSA.new(ENV['COJO_JWT_RSA_PRIVATE_KEY'])
+ end
+ end
+end
diff --git a/app/views/administrateurs/experts_procedures/index.html.haml b/app/views/administrateurs/experts_procedures/index.html.haml
index 8876f5b9ba0..a6889535208 100644
--- a/app/views/administrateurs/experts_procedures/index.html.haml
+++ b/app/views/administrateurs/experts_procedures/index.html.haml
@@ -10,7 +10,7 @@
.card
.card-title= t('.titles.allow_invite_experts')
- %p.notice= t('.descriptions.allow_invite_experts')
+ %p= t('.descriptions.allow_invite_experts')
= form_for @procedure,
method: :put,
url: allow_expert_review_admin_procedure_path(@procedure),
@@ -24,7 +24,7 @@
- if @procedure.allow_expert_review?
.card
.card-title= t('.titles.manage_procedure_experts')
- %p.notice= t('.descriptions.manage_procedure_experts')
+ %p= t('.descriptions.manage_procedure_experts')
= form_for @procedure,
method: :put,
url: experts_require_administrateur_invitation_admin_procedure_path(@procedure),
@@ -37,7 +37,7 @@
.card
.card-title= t('.titles.allow_expert_messaging')
- %p.notice= t('.descriptions.allow_expert_messaging')
+ %p= t('.descriptions.allow_expert_messaging')
= form_for @procedure,
method: :put,
url: allow_expert_messaging_admin_procedure_path(@procedure),
@@ -56,8 +56,8 @@
html: { class: 'form' } do |f|
.instructeur-wrapper
- %p.notice Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
- %p#experts-emails.notice Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
+ %p Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
+ %p#experts-emails Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
= hidden_field_tag :emails, nil
= react_component("ComboMultiple",
options: [],
diff --git a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
index bc2a07f12a6..0b94f472a28 100644
--- a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
+++ b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
@@ -4,7 +4,7 @@
= form_for :instructeur, url: { action: :add_instructeur, id: groupe_instructeur.id }, html: { class: 'form' } do |f|
.instructeur-wrapper
- if !procedure.routing_enabled?
- %p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
+ %p Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
- if disabled_as_super_admin
= f.select :emails, available_instructeur_emails, {}, disabled: disabled_as_super_admin, id: 'instructeur_emails'
diff --git a/app/views/administrateurs/procedures/_procedures_list.html.haml b/app/views/administrateurs/procedures/_procedures_list.html.haml
index 9fd38500bb1..b79af04c1e7 100644
--- a/app/views/administrateurs/procedures/_procedures_list.html.haml
+++ b/app/views/administrateurs/procedures/_procedures_list.html.haml
@@ -1,41 +1,70 @@
- procedures.each do |procedure|
.card
- .admin-procedures-list-row.infos.flex
- - if procedure.logo.present?
- = image_tag procedure.logo, alt: procedure.libelle, width: '100'
- .flex.column.ml-1
- .card-title
- = link_to procedure.libelle, admin_procedure_path(procedure), style: 'color: black;'
- = link_to commencer_url(procedure.path), commencer_url(procedure.path), class: 'fr-link fr-mb-1w'
-
- .admin-procedures-list-timestamps
- %p.notice N° #{procedure.id}
- %p.notice créée le #{procedure.created_at.strftime('%d/%m/%Y')}
- - if procedure.published_at.present?
- %p.notice publiée le #{procedure.published_at.strftime('%d/%m/%Y')}
-
- - if procedure.updated_at.today?
- %p.notice modifiée à #{procedure.updated_at.strftime('%H:%M')}
- - else
- %p.notice modifiée le #{procedure.updated_at.strftime('%d/%m/%Y %H:%M')}
+ .flex.justify-between
+ %div
+ .flex
+ - if procedure.logo.present?
+ = image_tag procedure.logo, alt: procedure.libelle, class: 'logo'
- - if procedure.closed_at.present?
- %p.notice archivée le #{procedure.closed_at.strftime('%d/%m/%Y')}
- - elsif procedure.auto_archive_on&.future?
- %p.notice sera clôturée le #{procedure.auto_archive_on.strftime('%d/%m/%Y')}
+ %div
+ .card-title
+ = link_to procedure.libelle, admin_procedure_path(procedure)
- .admin-procedures-list-row.actions.flex.justify-between
- %div
- - if procedure.routing_enabled?
- %span.icon.person
- %span.badge.baseline= procedure.groupe_instructeurs.count
- - else
- %span.icon.person
- %span.badge.baseline= procedure.instructeurs.count
+ = link_to commencer_url(procedure.path), commencer_url(procedure.path), class: 'fr-link fr-mb-1w'
+
+ %p.fr-mt-1w.fr-mb-1w
+ = t('administrateurs.procedures.created_at')
+ = procedure.created_at.strftime('%d/%m/%Y')
+
+ - if procedure.published_at.present?
+ %span
+ = t('administrateurs.procedures.published_at')
+ = procedure.published_at.strftime('%d/%m/%Y')
+
+ - if procedure.updated_at.today?
+ %span
+ = t('administrateurs.procedures.updated_at_today')
+ = procedure.updated_at.strftime('%H:%M')
+ - else
+ %span
+ = t('administrateurs.procedures.updated_at')
+ = procedure.updated_at.strftime('%d/%m/%Y %H:%M')
- %span.icon.folder
- %span.badge.baseline= procedure.dossiers.state_not_brouillon.visible_by_administration.count
+ - if procedure.closed_at.present?
+ %span
+ = t('administrateurs.procedures.closed_at')
+ = procedure.closed_at.strftime('%d/%m/%Y')
+ - elsif procedure.auto_archive_on&.future?
+ %span
+ = t('administrateurs.procedures.auto_archive_on')
+ = procedure.auto_archive_on.strftime('%d/%m/%Y')
+ %div
+ - if procedure.routing_enabled?
+ %span.icon.person
+ %span.fr-badge= procedure.groupe_instructeurs.count
+ - else
+ %span.icon.person
+ %span.fr-badge= procedure.instructeurs.count
+
+ %span.icon.folder.fr-ml-1w
+ %span.fr-badge= procedure.dossiers.state_not_brouillon.visible_by_administration.count
+
+ .text-right
+ %p.fr-mb-0 N° #{procedure.id}
+ - if procedure.close?
+ %span.fr-badge.fr-badge--sm.fr-badge--warning
+ = t('closed', scope: [:layouts, :breadcrumb])
+
+ - elsif procedure.locked?
+ %span.fr-badge.fr-badge--sm.fr-badge--success
+ = t('published', scope: [:layouts, :breadcrumb])
+
+ - else
+ %span.fr-badge.fr-badge--sm.fr-badge--new
+ = t('draft', scope: [:layouts, :breadcrumb])
+
+ .flex.justify-end
%ul.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
- unless procedure.discarded?
%li
@@ -80,4 +109,3 @@
%span.icon.unarchive
.dropdown-description
%h4= t('administrateurs.dropdown_actions.restore')
-
diff --git a/app/views/shared/champs/cojo/_show.html.haml b/app/views/shared/champs/cojo/_show.html.haml
new file mode 100644
index 00000000000..b5fa04c7844
--- /dev/null
+++ b/app/views/shared/champs/cojo/_show.html.haml
@@ -0,0 +1,9 @@
+= champ.to_s
+- if profile == 'instructeur'
+ %dl
+ %dt
+ Nom et prénom dans la base d’accréditation :
+ %dd= "#{champ.accreditation_last_name} #{champ.accreditation_first_name}"
+ %dt
+ Nom et prénom saisie dans le dossier :
+ %dd= "#{champ.dossier.individual.nom} #{champ.dossier.individual.prenom}"
diff --git a/app/views/shared/dossiers/_identite_entreprise.html.haml b/app/views/shared/dossiers/_identite_entreprise.html.haml
index 31ffaf157e9..79845792db7 100644
--- a/app/views/shared/dossiers/_identite_entreprise.html.haml
+++ b/app/views/shared/dossiers/_identite_entreprise.html.haml
@@ -28,8 +28,8 @@
- unless local_assigns[:short_identity]
- - if etablissement.siret != etablissement.entreprise.siret_siege_social
- = render Dossiers::RowShowComponent.new(label: "Numero TAHITI du siège social") do |c|
+ - if etablissement.entreprise.siret_siege_social.present? && etablissement.siret != etablissement.entreprise.siret_siege_social
+ = render Dossiers::RowShowComponent.new(label: "Numéro TAHITI du siège social") do |c|
- c.with_value do
%p
= pretty_siret(etablissement.entreprise.siret_siege_social)
diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml
index 1bedcba59f6..853a0c4ea35 100644
--- a/app/views/users/dossiers/_deleted_dossiers_list.html.haml
+++ b/app/views/users/dossiers/_deleted_dossiers_list.html.haml
@@ -1,38 +1,31 @@
- if deleted_dossiers.present?
- %span.fr-h6.fr-mr-2w
+ .fr-h6.fr-mb-2w
= page_entries_info deleted_dossiers
- .fr-table.fr-table--bordered.fr-table--no-caption.fr-mt-2w
- %table.table.dossiers-table.hoverable.display-table
- %caption= t('views.users.dossiers.dossiers_list.caption')
- %thead
- %tr
- %th.number-col Nº dossier
- %th Démarche
- %th Raison de suppression
- %th Date de suppression
- %tbody
- - deleted_dossiers.each do |dossier|
- %tr{ data: { 'dossier-id': dossier.dossier_id } }
- %td.number-col
- %span.icon.folder
- = dossier.dossier_id
- %td
- = dossier.procedure.libelle
+ - deleted_dossiers.each do |dossier|
+ .card
+ .flex.justify-between
+ %div
+ %h2.card-title
+ = dossier.procedure.libelle
- %td.cell-link
- = deletion_reason_badge(dossier.reason)
- %td
- = dossier.updated_at.strftime('%d/%m/%Y')
+ %p.fr-icon--sm.fr-icon-delete-line.fr-mb-0
+ = t('views.users.dossiers.dossiers_list.deleted', date: l(dossier.updated_at.to_date))
+ = "-"
+ = t("activerecord.attributes.deleted_dossier.reason.#{dossier.reason}")
- = paginate deleted_dossiers, views_prefix: 'shared'
+ .text-right
+ %p.fr-mb-0
+ = t('views.users.dossiers.dossiers_list.n_dossier')
+ = dossier.dossier_id
+
+ %span.fr-badge.fr-badge--sm.fr-badge--warning
+ = t('views.users.dossiers.dossiers_list.deleted_badge')
+
+ = paginate deleted_dossiers, views_prefix: 'shared'
- else
.blank-tab
- %h2.empty-text
- = t("views.users.dossiers.account_creation.empty")
- %p.empty-text-details
- = t("views.users.dossiers.account_creation.detail_one")
+ %h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title')
%p.empty-text-details
- = t("views.users.dossiers.account_creation.detail_two")
- #{APPLICATION_BASE_URL}/commencer/xxx.
+ = t('views.users.dossiers.dossiers_list.no_result_text_html', app_base: APPLICATION_BASE_URL)
diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml
index 03d8e741f9e..64b54de0950 100644
--- a/app/views/users/dossiers/_dossier_actions.html.haml
+++ b/app/views/users/dossiers/_dossier_actions.html.haml
@@ -7,23 +7,19 @@
- if has_actions
- = render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: {class: 'invite-user-actions'}, menu_options: {id: dom_id(dossier, :actions_menu)}, button_options: {class: 'fr-btn--sm'}) do |menu|
+ - if has_edit_action
+ - if dossier.brouillon?
+ = link_to t('views.users.dossiers.dossier_action.edit_draft'), (url_for_dossier(dossier)), class: 'fr-btn fr-btn--sm fr-mr-1w'
+
+ - else
+ = link_to t('views.users.dossiers.dossier_action.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn fr-btn--sm fr-btn--tertiary fr-mr-1w'
+
+ = render Dropdown::MenuComponent.new(wrapper: :div, wrapper_options: {class: 'invite-user-actions'}, menu_options: {id: dom_id(dossier, :actions_menu)}, button_options: {class: 'fr-btn--sm fr-btn--tertiary'}) do |menu|
- menu.with_button_inner_html do
- = t('views.users.dossiers.dossier_action.actions')
-
- - if has_edit_action
- - if dossier.brouillon?
- - menu.with_item do
- = link_to(url_for_dossier(dossier), role: 'menuitem') do
- %span.icon.edit
- .dropdown-description
- = t('views.users.dossiers.dossier_action.edit_draft')
+ - if has_edit_action
+ = t('views.users.dossiers.dossier_action.other_actions')
- else
- - menu.with_item do
- = link_to(modifier_dossier_path(dossier), role: 'menuitem') do
- %span.icon.edit
- .dropdown-description
- = t('views.users.dossiers.dossier_action.edit_dossier')
+ = t('views.users.dossiers.dossier_action.actions')
- if has_transfer_action
- menu.with_item do
diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml
index 4c5a7361aad..566c2dbd35e 100644
--- a/app/views/users/dossiers/_dossiers_list.html.haml
+++ b/app/views/users/dossiers/_dossiers_list.html.haml
@@ -1,53 +1,97 @@
- if dossiers.present?
- %span.fr-h6.fr-mr-2w
+ .fr-h6.fr-mb-2w
= page_entries_info dossiers
- .fr-table.fr-table--bordered.fr-table--no-caption.fr-mt-2w
- %table.table.dossiers-table.hoverable.hack-to-display-dropdown
- %caption= t('views.users.dossiers.dossiers_list.caption')
- %thead
- %tr
- %th.number-col{ scope: :col }= t('views.users.dossiers.dossiers_list.n_dossier')
- %th{ scope: :col }= t('views.users.dossiers.dossiers_list.procedure')
- - if dossiers.present?
- %th{ scope: :col }= t('views.users.dossiers.dossiers_list.requester')
- %th.status-col{ scope: :col }= t('views.users.dossiers.dossiers_list.status')
- %th.updated-at-col{ scope: :col }= t('views.users.dossiers.dossiers_list.updated')
- %th.action-col.follow-col{ scope: :col }= t('views.users.dossiers.dossiers_list.actions')
- %tbody
- - dossiers.each do |dossier|
- - if dossier.transfer.present?
- %tr.fr-background-alt--blue-france.no-border
- %td.fr-py-2w{ colspan: 100 }
- .flex.align-center
- %p.fr-mb-0
- %small
- = t('views.users.dossiers.transfers.sender_demande_en_cours', id: dossier.id, email: dossier.transfer.email)
- .ml-auto
- = link_to t('views.users.dossiers.transfers.revoke'), transfer_path(dossier.transfer), class: 'fr-btn fr-btn--sm fr-btn--tertiary-no-outline', method: :delete
-
- %tr{ data: { 'dossier-id': dossier.id } }
- %th.number-col{ scope: :row }
- = link_to(url_for_dossier(dossier), class: 'cell-link', tabindex: -1) do
- %span.icon.folder
- = dossier.id
- %td
+
+ - dossiers.each do |dossier|
+ .card
+ .flex.justify-between
+ %div
+ %h2.card-title
+ - if ["dossiers-transferes", "dossiers-supprimes-recemment"].exclude?(@statut)
= link_to(url_for_dossier(dossier), class: 'cell-link') do
- = procedure_libelle(dossier.procedure)
- - if dossiers.present?
- %td
- %span.cell-link= demandeur_dossier(dossier)
- %td.status-col
- - if dossier.pending_correction?
- = pending_correction_badge(:for_user)
+ = dossier.procedure.libelle
+ - else
+ = dossier.procedure.libelle
+
+ - if demandeur_dossier(dossier).present?
+ %p.fr-icon--sm.fr-icon-user-line
+ = demandeur_dossier(dossier)
+
+ - if dossier.hidden_by_user?
+ %p.fr-icon--sm.fr-icon-delete-line
+ = t('views.users.dossiers.dossiers_list.deleted', date: l(dossier.hidden_by_user_at.to_date))
+ - else
+ %p.fr-icon--sm.fr-icon-edit-box-line
+ - if dossier.depose_at.present?
+ %span
+ = t('views.users.dossiers.dossiers_list.depose_at', date: l(dossier.depose_at.to_date))
- else
- = status_badge(dossier.state)
+ %span
+ = t('views.users.dossiers.dossiers_list.created_at', date: l(dossier.created_at.to_date))
+ - if dossier.created_at != dossier.updated_at
+ = t('views.users.dossiers.dossiers_list.updated_at', date: l(dossier.updated_at.to_datetime))
+
+ - if dossier.invites.present?
+ %p.fr-icon--sm.fr-icon-shield-line
+ = t('views.users.dossiers.dossiers_list.shared_with')
+ = dossier.invites.map(&:email).join(', ')
+
+ .text-right
+ %p.fr-mb-0
+ = t('views.users.dossiers.dossiers_list.n_dossier')
+ = dossier.id
+
+ - if @statut == "dossiers-supprimes-recemment"
+ %span.fr-badge.fr-badge--sm.fr-badge--warning
+ = t('views.users.dossiers.dossiers_list.deleted_badge')
+ - else
+ = status_badge(dossier.state, 'fr-mb-1w')
+
+ - if dossier.pending_correction?
+ %br
+ = pending_correction_badge(:for_user)
+
+ - if dossier.procedure.close? && !dossier.termine?
+ = render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: "fr-mb-2w") do |c|
+ - c.body do
+ %p
+ = t('views.users.dossiers.dossiers_list.procedure_closed')
+
+ - if dossier.pending_correction?
+ = render Dsfr::AlertComponent.new(state: :warning, size: :sm, extra_class_names: "fr-mb-2w") do |c|
+ - c.body do
+ %p
+ = t('views.users.dossiers.dossiers_list.pending_correction')
+
+ - if dossier.transfer.present?
+ - if @statut == "dossiers-transferes"
+ = render Dsfr::AlertComponent.new(state: :info, size: :sm) do |c|
+ - c.body do
+ %p
+ = t('views.users.dossiers.transfers.receiver_demande_en_cours', id: dossier.id, email: dossier.user.email)
+ %p
+ = link_to t('views.users.dossiers.transfers.accept'), transfer_path(dossier.transfer), class: "fr-link fr-mr-1w", method: :put
+ = link_to t('views.users.dossiers.transfers.reject'), transfer_path(dossier.transfer), class: "fr-link", method: :delete
+ - else
+ = render Dsfr::AlertComponent.new(state: :info, size: :sm, extra_class_names: "fr-mb-2w") do |c|
+ - c.body do
+ %p
+ = t('views.users.dossiers.transfers.sender_demande_en_cours', id: dossier.id, email: dossier.transfer.email)
+ %p
+ = link_to t('views.users.dossiers.transfers.revoke'), transfer_path(dossier.transfer), class: 'fr-link', method: :delete
+
+
+ - if ["dossiers-transferes", "dossiers-supprimes-recemment"].exclude?(@statut)
+ .flex.justify-end
+ = render partial: 'dossier_actions', locals: { dossier: dossier }
+
+ - if @statut == "dossiers-supprimes-recemment"
+ .flex.justify-end
+ = link_to restore_dossier_path(dossier.id), method: :patch, class: "fr-btn fr-btn--sm" do
+ Restaurer
- %td.updated-at-col.cell-link
- = try_format_date(dossier.updated_at)
- %td.action-col.follow-col
- = render partial: 'dossier_actions', locals: { dossier: dossier }
+ = paginate dossiers, views_prefix: 'shared'
- = paginate dossiers, views_prefix: 'shared'
- else
- if filter.filter_params.present?
diff --git a/app/views/users/dossiers/_hidden_dossiers_list.html.haml b/app/views/users/dossiers/_hidden_dossiers_list.html.haml
deleted file mode 100644
index c5e63dee05e..00000000000
--- a/app/views/users/dossiers/_hidden_dossiers_list.html.haml
+++ /dev/null
@@ -1,44 +0,0 @@
-- if hidden_dossiers.present?
- %span.fr-h6.fr-mr-2w
- = page_entries_info hidden_dossiers
-
- .fr-table.fr-table--bordered.fr-table--no-caption.fr-mt-2w
- %table.table.dossiers-table.hoverable
- %caption= t('views.users.dossiers.dossiers_list.caption')
- %thead
- %tr
- %th.number-col Nº dossier
- %th Démarche
- %th Raison de suppression
- %th Date de suppression
- %th.action-col.follow-col Actions
- %tbody
- - hidden_dossiers.each do |dossier|
- - libelle_demarche = dossier.procedure.libelle
-
- %tr{ data: { 'dossier-id': dossier.id } }
- %td.number-col
- %span.icon.folder
- = dossier.id
- %td
- = libelle_demarche
-
- %td.cell-link
- = deletion_reason_badge("user_request")
- %td
- = dossier.updated_at.strftime('%d/%m/%Y')
- %td.action-col.follow-col
- = link_to restore_dossier_path(dossier.id), method: :patch, class: "fr-btn" do
- Restaurer
-
- = paginate hidden_dossiers, views_prefix: 'shared'
-
-- else
- .blank-tab
- %h2.empty-text
- = t("views.users.dossiers.account_creation.empty")
- %p.empty-text-details
- = t("views.users.dossiers.account_creation.detail_one")
- %p.empty-text-details
- = t("views.users.dossiers.account_creation.detail_two")
- #{APPLICATION_BASE_URL}/commencer/xxx.
diff --git a/app/views/users/dossiers/_transfered_dossiers_list.html.haml b/app/views/users/dossiers/_transfered_dossiers_list.html.haml
deleted file mode 100644
index f3aff1b8eed..00000000000
--- a/app/views/users/dossiers/_transfered_dossiers_list.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-- if dossier_transfers.present?
- .fr-table.fr-table--bordered
- %table.table.dossiers-table.display-table
- %thead
- %tr
- %th.number-col= t('views.users.dossiers.dossiers_list.n_dossier')
- %th= t('views.users.dossiers.dossiers_list.procedure')
- %th= t('views.users.dossiers.dossiers_list.status')
- %th.action-col.follow-col Date de dépot
- %tbody
- - dossier_transfers.each do |transfer|
- - transfer.dossiers.each do |dossier|
- %tr.fr-background-alt--blue-france.no-border
- %td.fr-py-2w{ colspan: 100 }
- .flex.align-center
- %p.fr-mb-0
- %small
- = t('views.users.dossiers.transfers.receiver_demande_en_cours', id: dossier.id, email: transfer.dossiers.first.user.email)
- .ml-auto
- = link_to t('views.users.dossiers.transfers.accept'), transfer_path(transfer), class: "fr-btn fr-btn--sm fr-btn--tertiary", method: :put
-
- = link_to t('views.users.dossiers.transfers.reject'), transfer_path(transfer), class: "fr-btn fr-btn--sm fr-btn--tertiary-no-outline", method: :delete
- %tr{ data: { 'transfer-id': transfer.id } }
- %th.number-col{ scope: :row }
- %span.icon.folder
- = dossier.id
- %td= dossier.procedure.libelle
- %td= status_badge(dossier.state)
- %td.action-col.follow-col{ style: 'padding: 18px;' }= (dossier.depose_at || dossier.created_at).strftime('%d/%m/%Y')
-
- = paginate dossier_transfers, views_prefix: 'shared'
-
-- else
- .blank-tab
- %h2.empty-text Aucune demande de transfert de dossiers ne vous a été adressée.
diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml
index 50b5790763e..0517f8a9e42 100644
--- a/app/views/users/dossiers/index.html.haml
+++ b/app/views/users/dossiers/index.html.haml
@@ -56,46 +56,32 @@
active: @statut == 'dossiers-supprimes-definitivement',
badge: number_with_html_delimiter(@dossiers_supprimes_definitivement.count))
- - if @dossier_transfers.present?
- = tab_item(t('pluralize.dossiers_transferes', count: @dossier_transfers.count),
+ - if @dossier_transferes.present?
+ = tab_item(t('pluralize.dossiers_transferes', count: @dossier_transferes.count),
dossiers_path(statut: 'dossiers-transferes'),
active: @statut == 'dossiers-transferes',
- badge: number_with_html_delimiter(@dossier_transfers.count))
+ badge: number_with_html_delimiter(@dossier_transferes.count))
.fr-container
- - if @statut == "en-cours"
- - if @first_brouillon_recently_updated.present?
- = render Dsfr::CalloutComponent.new(title: t('users.dossiers.header.callout.first_brouillon_recently_updated_title'), heading_level: 'h2') do |c|
- - c.with_body do
- %p
- = t('users.dossiers.header.callout.first_brouillon_recently_updated_text', time_ago: time_ago_in_words(@first_brouillon_recently_updated.created_at), libelle: @first_brouillon_recently_updated.procedure.libelle )
- = link_to t('users.dossiers.header.callout.first_brouillon_recently_updated_button'), url_for_dossier(@first_brouillon_recently_updated), class: 'fr-btn'
-
- - if @search_terms.present?
- %h2.page-title Résultat de la recherche pour « #{@search_terms} »
- = render partial: "dossiers_list", locals: { dossiers: @dossiers }
-
- - else
- = render Dossiers::UserFilterComponent.new(statut: @statut, filter: @filter)
-
- - if @statut == "en-cours"
- = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut }
-
- - if @statut == "traites"
- = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut }
-
- - if @statut == "dossiers-invites"
- = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut }
-
- - if @statut == "dossiers-supprimes-recemment"
- = render partial: "hidden_dossiers_list", locals: { hidden_dossiers: @dossiers }
-
- - if @statut == "dossiers-supprimes-definitivement"
- = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers }
-
- - if @statut == "dossiers-transferes"
- -# /!\ in this context, @dossiers is a collection of DossierTransfer not Dossier
- = render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossiers }
-
- - if @statut == "dossiers-expirant"
- = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut }
+ .fr-grid-row.fr-grid-row--center
+ .fr-col-xl-10
+ - if @statut == "en-cours"
+ - if @first_brouillon_recently_updated.present?
+ = render Dsfr::CalloutComponent.new(title: t('users.dossiers.header.callout.first_brouillon_recently_updated_title'), heading_level: 'h2') do |c|
+ - c.with_body do
+ %p
+ = t('users.dossiers.header.callout.first_brouillon_recently_updated_text', time_ago: time_ago_in_words(@first_brouillon_recently_updated.created_at), libelle: @first_brouillon_recently_updated.procedure.libelle )
+ = link_to t('users.dossiers.header.callout.first_brouillon_recently_updated_button'), url_for_dossier(@first_brouillon_recently_updated), class: 'fr-btn'
+
+ - if @search_terms.present?
+ %h2.page-title Résultat de la recherche pour « #{@search_terms} »
+ = render partial: "dossiers_list", locals: { dossiers: @dossiers }
+
+ - else
+ = render Dossiers::UserFilterComponent.new(statut: @statut, filter: @filter)
+
+ - if @statut == "dossiers-supprimes-definitivement"
+ -# /!\ in this context, @dossiers is a collection of DeletedDossier not Dossier
+ = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers }
+ - else
+ = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut }
diff --git a/config/env.example.optional b/config/env.example.optional
index c8e36117071..2a905ab1fb0 100644
--- a/config/env.example.optional
+++ b/config/env.example.optional
@@ -201,3 +201,9 @@ BANNER_MESSAGE=""
ADMINISTRATION_BANNER_MESSAGE=""
# for usager only
USAGER_BANNER_MESSAGE=""
+
+# RSA private key to generate JWT tokens for communication with COJO services
+COJO_JWT_RSA_PRIVATE_KEY=""
+COJO_JWT_ISS=""
+
+API_COJO_URL=""
diff --git a/config/initializers/02_urls.rb b/config/initializers/02_urls.rb
index 34145a6e328..e2936cff73e 100644
--- a/config/initializers/02_urls.rb
+++ b/config/initializers/02_urls.rb
@@ -6,6 +6,7 @@
API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr")
API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
API_TCHAP_URL = ENV.fetch("API_TCHAP_URL", "https://matrix.agent.tchap.gouv.fr/_matrix/identity/api/v1")
+API_COJO_URL = ENV.fetch("API_COJO_URL", nil)
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb
index bea706177ab..042333ae3fa 100644
--- a/config/initializers/active_storage.rb
+++ b/config/initializers/active_storage.rb
@@ -10,7 +10,7 @@
def self.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
token = super
- "#{Time.current.year}/#{token[0..1]}/#{token[2..3]}/#{token}"
+ "#{Time.current.strftime('%Y/%m/%d')}/#{token[0..1]}/#{token}"
end
end
diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb
index 13b669a3e41..99357cd4597 100644
--- a/config/initializers/flipper.rb
+++ b/config/initializers/flipper.rb
@@ -21,7 +21,8 @@ def setup_features(features)
:attestation_v2,
:procedure_routage_api,
:groupe_instructeur_api_hack,
- :rerouting
+ :rerouting,
+ :cojo_type_de_champ
]
def database_exists?
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index dc982817a33..3e3a763beba 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -8,6 +8,7 @@
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
+ inflect.acronym 'COJO'
inflect.acronym 'API'
inflect.acronym 'ASN1'
inflect.acronym 'IP'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 26a7e4b270f..8d23bd28c0d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -396,10 +396,6 @@ en:
dossier_not_in_instructor_group: "File no. %{dossier_id} of the “%{procedure_libelle}” procedure corresponds to your search, but it is attached to the “%{groupe_instructeur_label}” instructor group."
users:
dossiers:
- account_creation:
- empty: "No file"
- detail_one: "To complete a procedure, contact your administration and ask for the link to the procedure."
- detail_two: "This one should look like"
fix_champ: "fill in this field"
label_champ: "« %{champ} » field %{message}"
archived_dossier: "Your file will be kept %{duree_conservation_dossiers_dans_ds} more months"
@@ -464,17 +460,19 @@ en:
index:
dossiers: "My files"
dossiers_list:
- caption: My files
- procedure: "Procedure"
n_dossier: "File n."
- requester: "Requester"
- status: "Status"
- updated: "Updated"
- actions: "Actions"
no_result_title: No files
no_result_text_html: "To fill a procedure, contact your administration asking for the procedure link.
It should look like %{app_base}/commencer/xxx."
no_result_text_with_filter: found with selected filters
no_result_reset_filter: Reset filters
+ procedure_closed: This procedure has been closed, you will not be able to submit a file again from the procedure link, contact your administration for more information.
+ pending_correction: This procedure is awaiting your corrections. Correct the fields that are notified by an alert message in the form.
+ depose_at: First submission on %{date}
+ created_at: Created at %{date}
+ updated_at: updated at %{date}
+ shared_with: File shared with
+ deleted: Deleted at %{date}
+ deleted_badge: Deleted
transfers:
sender_demande_en_cours: "A transfer request is pending on file Nº %{id} to %{email}"
receiver_demande_en_cours: "Transfer request on file Nº %{id} sent by %{email}"
@@ -487,8 +485,9 @@ en:
clone: "Duplicate the file"
delete_dossier: "Delete the file"
transfer_dossier: "Transfer the file"
- edit_draft: "Edit the draft"
+ edit_draft: "Keep filling"
actions: "Actions"
+ other_actions: "Other actions"
sessions:
new:
sign_in: "Connect with %{application_name} account"
@@ -772,6 +771,13 @@ en:
new:
title: Pick a password
continue: Continue
+ procedures:
+ created_at: created at
+ published_at: published at
+ updated_at_today: updated at
+ updated_at: updated at
+ closed_at: closed at
+ auto_archive_on: will close at
users:
activate:
new:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 3edad00dfda..124bb3e3057 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -397,10 +397,6 @@ fr:
dossier_not_in_instructor_group: "Le dossier n° %{dossier_id} de la procédure « %{procedure_libelle} » correspond à votre recherche mais il est rattaché au groupe d’instructeurs « %{groupe_instructeur_label} »."
users:
dossiers:
- account_creation:
- empty: "Aucun dossier"
- detail_one: "Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche."
- detail_two: "Celui ci doit ressembler à"
fix_champ: "corriger l’erreur"
label_champ: "Le champ « %{champ} » %{message}"
archived_dossier: "Votre dossier sera conservé %{duree_conservation_dossiers_dans_ds} mois supplémentaire"
@@ -465,25 +461,28 @@ fr:
index:
dossiers: "Mes dossiers"
dossiers_list:
- caption: Mes dossiers
- procedure: "Démarche"
n_dossier: "Nº dossier"
- requester: "Demandeur"
- status: "Statut"
- updated: "Mis à jour"
- actions: "Actions"
no_result_title: Aucun dossier
no_result_text_html: "Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche.
Celui ci doit ressembler à %{app_base}/commencer/xxx."
no_result_text_with_filter: ne correspond aux filtres sélectionnés
no_result_reset_filter: Réinitialiser les filtres
+ procedure_closed: Cette démarche a été clôturée, vous ne pourrez pas redéposer de dossier à partir du lien de la démarche, contactez votre administration pour plus d’information.
+ pending_correction: Cette démarche est en attente de vos corrections. Corriger les champs qui sont notifiés par un message d’alerte dans le formulaire.
+ depose_at: Déposé le %{date}
+ created_at: Créé le %{date}
+ updated_at: modifié le %{date}
+ shared_with: Dossier partagé avec
+ deleted: Supprimé le %{date}
+ deleted_badge: Supprimé
dossier_action:
edit_dossier: "Modifier le dossier"
start_other_dossier: "Commencer un autre dossier vide"
clone: "Dupliquer ce dossier"
delete_dossier: "Supprimer le dossier"
transfer_dossier: "Transférer le dossier"
- edit_draft: "Modifier le brouillon"
+ edit_draft: "Continuer à remplir"
actions: "Actions"
+ other_actions: "Autres actions"
transfers:
sender_demande_en_cours: "Une demande de transfert est en cours sur le dossier Nº %{id} pour %{email}"
receiver_demande_en_cours: "Demande de transfert pour le dossier Nº %{id} envoyé par %{email}"
@@ -856,6 +855,12 @@ fr:
to_clone: Cloner
to_close: Clore
procedures:
+ created_at: créée le
+ published_at: publiée le
+ updated_at_today: modifiée à
+ updated_at: modifiée le
+ closed_at: archivée le
+ auto_archive_on: sera clôturée le
show:
ready: "Validé"
needs_configuration: "À configurer"
diff --git a/config/locales/models/deleted_dossier/en.yml b/config/locales/models/deleted_dossier/en.yml
new file mode 100644
index 00000000000..c2ab4153381
--- /dev/null
+++ b/config/locales/models/deleted_dossier/en.yml
@@ -0,0 +1,16 @@
+en:
+ activerecord:
+ models:
+ deleted_dossier:
+ one: "Deleted file"
+ other: "Deleted files"
+ attributes:
+ deleted_dossier:
+ reason:
+ user_request: User request
+ manager_request: Manager request
+ user_removed: User removed
+ procedure_removed: Procedure removed
+ expired: Expired
+ unknown: Unknow
+ instructeur_request: Deleted by instructor
diff --git a/config/locales/models/deleted_dossier/fr.yml b/config/locales/models/deleted_dossier/fr.yml
index 87dd5a670bf..4f9664df7df 100644
--- a/config/locales/models/deleted_dossier/fr.yml
+++ b/config/locales/models/deleted_dossier/fr.yml
@@ -1,5 +1,9 @@
fr:
activerecord:
+ models:
+ deleted_dossier:
+ one: "Dossier supprimé"
+ other: "Dossiers supprimés"
attributes:
deleted_dossier:
reason:
diff --git a/config/locales/models/type_de_champ/en.yml b/config/locales/models/type_de_champ/en.yml
index 379a7928b85..0429c3f656d 100644
--- a/config/locales/models/type_de_champ/en.yml
+++ b/config/locales/models/type_de_champ/en.yml
@@ -50,6 +50,7 @@ en:
pole_emploi: 'Pôle emploi status'
mesri: "Data from Ministère de l’Enseignement Supérieur, de la Recherche et de l’Innovation"
epci: "EPCI"
+ cojo: "Accreditation Paris 2024"
errors:
type_de_champ:
attributes:
diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml
index 6d1a7f24cc8..4a0f8236a1a 100644
--- a/config/locales/models/type_de_champ/fr.yml
+++ b/config/locales/models/type_de_champ/fr.yml
@@ -56,6 +56,7 @@ fr:
pole_emploi: 'Situation Pôle emploi'
mesri: "Données du Ministère de l’Enseignement Supérieur, de la Recherche et de l’Innovation"
epci: "EPCI"
+ cojo: "Accréditation Paris 2024"
errors:
type_de_champ:
attributes:
diff --git a/config/routes.rb b/config/routes.rb
index 7454f583789..37542d33934 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -337,6 +337,7 @@
get 'modifier', to: 'dossiers#modifier'
post 'modifier', to: 'dossiers#submit_en_construction'
patch 'modifier', to: 'dossiers#modifier_legacy'
+ get 'champs/:champ_id', to: 'dossiers#champ', as: :champ
get 'merci'
get 'demande'
get 'messagerie'
@@ -450,6 +451,7 @@
get 'avis'
get 'avis_new'
get 'personnes-impliquees' => 'dossiers#personnes_impliquees'
+ get 'annotations/:annotation_id', to: 'dossiers#annotation', as: :annotation
patch 'follow'
patch 'unfollow'
patch 'archive'
diff --git a/spec/controllers/champs/siret_controller_spec.rb b/spec/controllers/champs/siret_controller_spec.rb
index 47123ec2ed7..c181139e588 100644
--- a/spec/controllers/champs/siret_controller_spec.rb
+++ b/spec/controllers/champs/siret_controller_spec.rb
@@ -133,7 +133,7 @@
end
end
- context 'when the Numero TAHITI is valid but unknown', vcr: { cassette_name: 'pf_api_entreprise_not_found' } do
+ context 'when the Numéro TAHITI is valid but unknown', vcr: { cassette_name: 'pf_api_entreprise_not_found' } do
let(:siret) { '111111' }
let(:api_etablissement_status) { 404 }
diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb
index a0f8f09b6fb..05dafa2f59d 100644
--- a/spec/factories/champ.rb
+++ b/spec/factories/champ.rb
@@ -271,6 +271,10 @@
value { 'W173847273' }
end
+ factory :champ_cojo, class: 'Champs::COJOChamp' do
+ type_de_champ { association :type_de_champ_cojo, procedure: dossier.procedure }
+ end
+
factory :champ_repetition, class: 'Champs::RepetitionChamp' do
type_de_champ { association :type_de_champ_repetition, procedure: dossier.procedure }
diff --git a/spec/factories/type_de_champ.rb b/spec/factories/type_de_champ.rb
index 1a485e85aae..87a03441825 100644
--- a/spec/factories/type_de_champ.rb
+++ b/spec/factories/type_de_champ.rb
@@ -199,6 +199,9 @@
factory :type_de_champ_epci do
type_champ { TypeDeChamp.type_champs.fetch(:epci) }
end
+ factory :type_de_champ_cojo do
+ type_champ { TypeDeChamp.type_champs.fetch(:cojo) }
+ end
factory :type_de_champ_repetition do
type_champ { TypeDeChamp.type_champs.fetch(:repetition) }
mandatory { true }
diff --git a/spec/fixtures/files/api_cojo/accreditation_invalid.json b/spec/fixtures/files/api_cojo/accreditation_invalid.json
new file mode 100644
index 00000000000..31b28dc950d
--- /dev/null
+++ b/spec/fixtures/files/api_cojo/accreditation_invalid.json
@@ -0,0 +1,4 @@
+{
+ "individualExistance": "maybe",
+ "firstName": "Florence"
+}
diff --git a/spec/fixtures/files/api_cojo/accreditation_no.json b/spec/fixtures/files/api_cojo/accreditation_no.json
new file mode 100644
index 00000000000..c849c997c21
--- /dev/null
+++ b/spec/fixtures/files/api_cojo/accreditation_no.json
@@ -0,0 +1,5 @@
+{
+ "individualExistance": "No",
+ "firstName": "Not found",
+ "lastName": "Not found"
+}
diff --git a/spec/fixtures/files/api_cojo/accreditation_yes.json b/spec/fixtures/files/api_cojo/accreditation_yes.json
new file mode 100644
index 00000000000..1d54bc0a7cc
--- /dev/null
+++ b/spec/fixtures/files/api_cojo/accreditation_yes.json
@@ -0,0 +1,5 @@
+{
+ "individualExistance": "Yes",
+ "firstName": "Florence",
+ "lastName": "Griffith-Joyner"
+}
diff --git a/spec/jobs/champ_fetch_external_data_job_spec.rb b/spec/jobs/champ_fetch_external_data_job_spec.rb
index 6f5277727ca..a4f75cc67e8 100644
--- a/spec/jobs/champ_fetch_external_data_job_spec.rb
+++ b/spec/jobs/champ_fetch_external_data_job_spec.rb
@@ -3,17 +3,21 @@
require 'rails_helper'
RSpec.describe ChampFetchExternalDataJob, type: :job do
- let(:champ) { Struct.new(:external_id, :data).new(champ_external_id, data) }
+ let(:champ) { build(:champ, external_id: champ_external_id, data:) }
let(:external_id) { "an ID" }
let(:champ_external_id) { "an ID" }
let(:data) { nil }
let(:fetched_data) { nil }
+ let(:reason) { StandardError.new("error") }
subject(:perform_job) { described_class.perform_now(champ, external_id) }
+ include Dry::Monads[:result]
+
before do
allow(champ).to receive(:fetch_external_data).and_return(fetched_data)
allow(champ).to receive(:update_with_external_data!)
+ allow(champ).to receive(:log_fetch_external_data_exception)
end
shared_examples "a champ non-updater" do
@@ -38,6 +42,35 @@
end
end
+ context 'when the fetched data is a result' do
+ context 'success' do
+ let(:fetched_data) { Success("data") }
+
+ it 'updates the champ' do
+ perform_job
+ expect(champ).to have_received(:update_with_external_data!).with(data: fetched_data.value!)
+ end
+ end
+
+ context 'retryable failure' do
+ let(:fetched_data) { Failure(API::Client::Error[:http, 400, true, reason]) }
+
+ it 'saves exception and raise' do
+ expect { perform_job }.to raise_error StandardError
+ expect(champ).to have_received(:log_fetch_external_data_exception).with(reason)
+ end
+ end
+
+ context 'fatal failure' do
+ let(:fetched_data) { Failure(API::Client::Error[:http, 400, false, reason]) }
+
+ it 'saves exception' do
+ perform_job
+ expect(champ).to have_received(:log_fetch_external_data_exception).with(reason)
+ end
+ end
+ end
+
context 'when the fetched data is blank' do
it_behaves_like "a champ non-updater"
end
diff --git a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb
index da9690ae729..e9dad94b132 100644
--- a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb
+++ b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb
@@ -3,7 +3,7 @@
let(:procedure) { create(:procedure, :with_all_champs) }
let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) }
let(:champ_repetition) { dossier.champs_public.find(&:repetition?) }
- let(:champ_count) { 46 }
+ let(:champ_count) { 47 }
subject(:run_task) do
dossier
diff --git a/spec/models/champs/cojo_champ_spec.rb b/spec/models/champs/cojo_champ_spec.rb
new file mode 100644
index 00000000000..844d8d5e3a1
--- /dev/null
+++ b/spec/models/champs/cojo_champ_spec.rb
@@ -0,0 +1,50 @@
+describe Champs::COJOChamp, type: :model do
+ let(:champ) { build(:champ_cojo, accreditation_number:, accreditation_birthdate:) }
+ let(:external_id) { nil }
+ let(:stub) { stub_request(:post, url).with(body: { accreditationNumber: accreditation_number, birthdate: accreditation_birthdate }).to_return(body:, status:) }
+ let(:url) { COJOService.new.send(:url) }
+ let(:body) { Rails.root.join('spec', 'fixtures', 'files', 'api_cojo', "accreditation_#{response_type}.json").read }
+ let(:status) { 200 }
+ let(:response_type) { 'yes' }
+ let(:accreditation_number) { 123456 }
+ let(:accreditation_birthdate) { '21/12/1959' }
+
+ describe 'fetch_external_data' do
+ subject { stub; champ.fetch_external_data }
+
+ context 'success (yes)' do
+ it { expect(subject.value!).to eq({ accreditation_success: true, accreditation_first_name: 'Florence', accreditation_last_name: 'Griffith-Joyner' }) }
+ end
+
+ context 'success (no)' do
+ let(:response_type) { 'no' }
+ it { expect(subject.value!).to eq({ accreditation_success: false, accreditation_first_name: nil, accreditation_last_name: nil }) }
+ end
+
+ context 'failure (schema)' do
+ let(:response_type) { 'invalid' }
+ it {
+ expect(subject.failure.retryable).to be_falsey
+ expect(subject.failure.reason).to be_a(API::Client::SchemaError)
+ }
+ end
+
+ context 'failure (http 500)' do
+ let(:status) { 500 }
+ let(:response_type) { 'invalid' }
+ it {
+ expect(subject.failure.retryable).to be_truthy
+ expect(subject.failure.reason).to be_a(API::Client::HTTPError)
+ }
+ end
+
+ context 'failure (http 401)' do
+ let(:status) { 401 }
+ let(:response_type) { 'invalid' }
+ it {
+ expect(subject.failure.retryable).to be_falsey
+ expect(subject.failure.reason).to be_a(API::Client::HTTPError)
+ }
+ end
+ end
+end
diff --git a/spec/system/administrateurs/procedure_cloning_spec.rb b/spec/system/administrateurs/procedure_cloning_spec.rb
index 938b69d7c82..7fd1017dbb6 100644
--- a/spec/system/administrateurs/procedure_cloning_spec.rb
+++ b/spec/system/administrateurs/procedure_cloning_spec.rb
@@ -35,7 +35,7 @@
scenario do
visit admin_procedures_path
expect(page.find_by_id('procedures')['data-item-count']).to eq('1')
- page.all('.admin-procedures-list-row .dropdown .fr-btn').first.click
+ page.all('.card .dropdown .fr-btn').first.click
page.all('.clone-btn').first.click
visit admin_procedures_path(statut: "brouillons")
expect(page.find_by_id('procedures')['data-item-count']).to eq('1')
diff --git a/spec/system/routing/rules_full_scenario_spec.rb b/spec/system/routing/rules_full_scenario_spec.rb
index 5b9d9cb1201..3d04b926dde 100644
--- a/spec/system/routing/rules_full_scenario_spec.rb
+++ b/spec/system/routing/rules_full_scenario_spec.rb
@@ -66,18 +66,18 @@
expect(page).to have_text('Le nom est à présent « littéraire ». ')
# add victor to littéraire groupe
- fill_in 'Emails', with: 'victor@inst.com'
+ fill_in 'Emails', with: 'victor@gouv.fr'
perform_enqueued_jobs { click_on 'Affecter' }
- expect(page).to have_text("L’instructeur victor@inst.com a été affecté")
+ expect(page).to have_text("L’instructeur victor@gouv.fr a été affecté")
- victor = User.find_by(email: 'victor@inst.com').instructeur
+ victor = User.find_by(email: 'victor@gouv.fr').instructeur
- # add superwoman to littéraire groupe
- fill_in 'Emails', with: 'superwoman@inst.com'
+ # add alain to littéraire groupe
+ fill_in 'Emails', with: 'alain@gouv.fr'
perform_enqueued_jobs { click_on 'Affecter' }
- expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
+ expect(page).to have_text("L’instructeur alain@gouv.fr a été affecté")
- superwoman = User.find_by(email: 'superwoman@inst.com').instructeur
+ alain = User.find_by(email: 'alain@gouv.fr').instructeur
# add inactive groupe
click_on 'Ajout de groupes'
@@ -94,16 +94,16 @@
expect(page).to have_text('Le nom est à présent « scientifique ». ')
# add marie to scientifique groupe
- fill_in 'Emails', with: 'marie@inst.com'
+ fill_in 'Emails', with: 'marie@gouv.fr'
perform_enqueued_jobs { click_on 'Affecter' }
- expect(page).to have_text("L’instructeur marie@inst.com a été affecté")
+ expect(page).to have_text("L’instructeur marie@gouv.fr a été affecté")
- marie = User.find_by(email: 'marie@inst.com').instructeur
+ marie = User.find_by(email: 'marie@gouv.fr').instructeur
# add superwoman to scientifique groupe
- fill_in 'Emails', with: 'superwoman@inst.com'
+ fill_in 'Emails', with: 'alain@gouv.fr'
perform_enqueued_jobs { click_on 'Affecter' }
- expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
+ expect(page).to have_text("L’instructeur alain@gouv.fr a été affecté")
# add routing rules
within('.target') { select('Spécialité') }
@@ -183,7 +183,7 @@
visit new_user_session_path
sign_in_with litteraire_user.email, password
- click_on litteraire_user.dossiers.first.id.to_s
+ click_on litteraire_user.dossiers.first.procedure.libelle
click_on 'Modifier mon dossier'
fill_in litteraire_user.dossiers.first.champs_public.first.libelle, with: 'some value'
@@ -223,7 +223,7 @@
log_out
# the instructeurs who belong to scientifique AND litteraire groups manage scientifique and litteraire dossiers
- register_instructeur_and_log_in(superwoman.email)
+ register_instructeur_and_log_in(alain.email)
visit instructeur_procedure_path(procedure, params: { statut: 'tous' })
expect(page).to have_text(litteraire_user.email)
expect(page).to have_text(scientifique_user.email)
@@ -242,7 +242,7 @@
# the instructeurs who belong to scientifique AND litteraire groups should have a notification
visit new_user_session_path
- sign_in_with superwoman.user.email, password
+ sign_in_with alain.user.email, password
expect(page).to have_current_path(instructeur_procedures_path)
expect(find('.procedure-stats')).to have_css('span.notifications')
@@ -282,7 +282,7 @@ def user_send_dossier(user, groupe)
def user_update_group(user, new_group)
login_as user, scope: :user
visit dossiers_path
- click_on user.dossiers.first.id.to_s
+ click_on user.dossiers.first.procedure.libelle
click_on "Modifier mon dossier"
choose(new_group)
diff --git a/spec/system/users/invite_spec.rb b/spec/system/users/invite_spec.rb
index 917a54068a4..4fcac39bcf4 100644
--- a/spec/system/users/invite_spec.rb
+++ b/spec/system/users/invite_spec.rb
@@ -149,13 +149,13 @@ def log_in(user)
def navigate_to_brouillon(dossier)
expect(page).to have_current_path(dossiers_path)
- click_on(dossier.id.to_s)
+ click_on(dossier.procedure.libelle)
expect(page).to have_current_path(brouillon_dossier_path(dossier))
end
def navigate_to_dossier(dossier)
expect(page).to have_current_path(dossiers_path)
- click_on(dossier.id.to_s)
+ click_on(dossier.procedure.libelle)
expect(page).to have_current_path(dossier_path(dossier))
end
diff --git a/spec/system/users/list_dossiers_spec.rb b/spec/system/users/list_dossiers_spec.rb
index 109a01aace7..a0d4e4e7ec5 100644
--- a/spec/system/users/list_dossiers_spec.rb
+++ b/spec/system/users/list_dossiers_spec.rb
@@ -157,15 +157,16 @@
context 'when user clicks on delete button', js: true do
scenario 'the dossier is deleted' do
- within(:css, "tr[data-dossier-id=\"#{dossier_brouillon.id}\"]") do
- click_on 'Actions'
+ expect(page).to have_content(dossier_en_construction.procedure.libelle)
+ within(:css, ".card", match: :first) do
+ click_on 'Autres actions'
page.accept_alert('Confirmer la suppression ?') do
click_on 'Supprimer le dossier'
end
end
expect(page).to have_content('Votre dossier a bien été supprimé')
- expect(page).not_to have_content(dossier_brouillon.procedure.libelle)
+ expect(page).not_to have_content(dossier_en_construction.procedure.libelle)
end
end
@@ -177,10 +178,10 @@
end
context 'when user clicks on clone button', js: true do
- scenario 'the dossier is deleted' do
- within(:css, "tr[data-dossier-id=\"#{dossier_brouillon.id}\"]") do
- click_on 'Actions'
- click_on 'Dupliquer ce dossier'
+ scenario 'the dossier is cloned' do
+ within(:css, ".card", match: :first) do
+ click_on 'Autres actions'
+ expect { click_on 'Dupliquer ce dossier' }.to change { dossier_brouillon.user.dossiers.count }.by(1)
end
expect(page).to have_content("Votre dossier a bien été dupliqué. Vous pouvez maintenant le vérifier, l’adapter puis le déposer.")
diff --git a/spec/system/users/transfer_dossier_spec.rb b/spec/system/users/transfer_dossier_spec.rb
index 8ef0704271b..2e2622dcd97 100644
--- a/spec/system/users/transfer_dossier_spec.rb
+++ b/spec/system/users/transfer_dossier_spec.rb
@@ -11,8 +11,8 @@
end
scenario 'the user can transfer dossier to another user' do
- within(:css, "tr[data-dossier-id=\"#{dossier.id}\"]") do
- click_on 'Actions'
+ within(:css, ".card", match: :first) do
+ click_on 'Autres actions'
click_on 'Transférer le dossier'
end
diff --git a/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb b/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb
index 845bf5d94eb..84a8e735e27 100644
--- a/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb
+++ b/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb
@@ -56,5 +56,35 @@
expect(rendered).to include("9 001")
end
end
+
+ context 'siret siege social' do
+ let(:etablissement) { create(:etablissement, siret: "12345678900001", entreprise_siret_siege_social: siret_siege_social) }
+
+ context 'when siege social has same siret' do
+ let(:siret_siege_social) { "12345678900001" }
+
+ it 'does not duplicate siret' do
+ expect(subject).to include("123 456 789 00001").once
+ end
+ end
+
+ context 'when siege social is different' do
+ let(:siret_siege_social) { "98765432100001" }
+
+ it 'shows both sirets' do
+ expect(subject).to include("123 456 789 00001")
+ expect(subject).to include("Numéro TAHITI du siège social")
+ expect(subject).to include("987 654 321 00001")
+ end
+ end
+
+ context 'when siege social has no siret' do
+ let(:siret_siege_social) { nil }
+
+ it 'does not duplicate siret' do
+ expect(subject).not_to include("Numéro TAHITI du siège social")
+ end
+ end
+ end
end
end
diff --git a/spec/views/users/dossiers/index.html.haml_spec.rb b/spec/views/users/dossiers/index.html.haml_spec.rb
index 8cb30522f3e..0b34cf2b40b 100644
--- a/spec/views/users/dossiers/index.html.haml_spec.rb
+++ b/spec/views/users/dossiers/index.html.haml_spec.rb
@@ -16,7 +16,7 @@
assign(:dossiers_supprimes_recemment, Kaminari.paginate_array(user_dossiers).page(1))
assign(:dossiers_supprimes_definitivement, Kaminari.paginate_array(user_dossiers).page(1))
assign(:dossiers_traites, Kaminari.paginate_array(user_dossiers).page(1))
- assign(:dossier_transfers, Kaminari.paginate_array([]).page(1))
+ assign(:dossier_transferes, Kaminari.paginate_array([]).page(1))
assign(:dossiers_close_to_expiration, Kaminari.paginate_array([]).page(1))
assign(:dossiers, Kaminari.paginate_array(user_dossiers).page(1))
assign(:statut, statut)
@@ -24,19 +24,19 @@
render
end
- it 'affiche la liste des dossiers' do
- expect(rendered).to have_selector('.dossiers-table tbody tr', count: 3)
+ it 'affiche les dossiers' do
+ expect(rendered).to have_selector('.card', count: 3)
end
it 'affiche les informations des dossiers' do
dossier = user_dossiers.first
expect(rendered).to have_text(dossier_brouillon.id.to_s)
expect(rendered).to have_text(dossier_brouillon.procedure.libelle)
- expect(rendered).to have_link(dossier_brouillon.id.to_s, href: brouillon_dossier_path(dossier_brouillon))
+ expect(rendered).to have_link(dossier_brouillon.procedure.libelle, href: brouillon_dossier_path(dossier_brouillon))
expect(rendered).to have_text(dossier_en_construction.id.to_s)
expect(rendered).to have_text(dossier_en_construction.procedure.libelle)
- expect(rendered).to have_link(dossier_en_construction.id.to_s, href: dossier_path(dossier_en_construction))
+ expect(rendered).to have_link(dossier_en_construction.procedure.libelle, href: dossier_path(dossier_en_construction))
end
it 'n’affiche pas une alerte pour continuer à remplir un dossier' do