From 1f5b551b24feb3ada4f1fd9893fa5b8a38cd3cc6 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Mon, 26 Jun 2023 18:27:40 +0200 Subject: [PATCH 01/21] change design from table to cards for usager dashboard --- app/assets/stylesheets/card.scss | 14 +-- app/assets/stylesheets/flex.scss | 4 + app/components/dsfr/alert_component.rb | 11 +- .../alert_component/alert_component.html.haml | 5 +- .../experts_procedures/index.html.haml | 10 +- .../_instructeurs.html.haml | 2 +- .../procedures/_procedures_list.html.haml | 17 ++- .../users/dossiers/_dossier_actions.html.haml | 27 ++--- .../users/dossiers/_dossiers_list.html.haml | 109 ++++++++++-------- config/locales/en.yml | 12 +- config/locales/fr.yml | 12 +- 11 files changed, 127 insertions(+), 96 deletions(-) diff --git a/app/assets/stylesheets/card.scss b/app/assets/stylesheets/card.scss index 2db51bef097..5e528b45399 100644 --- a/app/assets/stylesheets/card.scss +++ b/app/assets/stylesheets/card.scss @@ -6,18 +6,18 @@ border: 1px solid $border-grey; margin-bottom: $default-spacer * 2; background: #FFFFFF; - - .notice { - font-size: 16px; - color: #666666; - margin-top: -8px; - margin-bottom: 16px; - } + color: $dark-grey; .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 + } } &.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/dsfr/alert_component.rb b/app/components/dsfr/alert_component.rb index d1fe2856620..42ed9f432fc 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 + if size == 'small' + ["fr-alert fr-alert--sm fr-alert--#{state}", extra_class_names].compact.flatten + else + ["fr-alert fr-alert--#{state}", extra_class_names].compact.flatten + end 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..a0f9cf28d85 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 != 'small' + = content_tag(heading_level, class: 'fr-alert__title') do + = "#{prefix_for_state}#{title}" = body 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..ede6aa56d62 100644 --- a/app/views/administrateurs/procedures/_procedures_list.html.haml +++ b/app/views/administrateurs/procedures/_procedures_list.html.haml @@ -5,24 +5,24 @@ = 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 procedure.libelle, admin_procedure_path(procedure) = 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')} + %p N° #{procedure.id} + %p 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')} + %p 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')} + %p modifiée à #{procedure.updated_at.strftime('%H:%M')} - else - %p.notice modifiée le #{procedure.updated_at.strftime('%d/%m/%Y %H:%M')} + %p modifiée le #{procedure.updated_at.strftime('%d/%m/%Y %H:%M')} - if procedure.closed_at.present? - %p.notice archivée le #{procedure.closed_at.strftime('%d/%m/%Y')} + %p 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')} + %p sera clôturée le #{procedure.auto_archive_on.strftime('%d/%m/%Y')} .admin-procedures-list-row.actions.flex.justify-between %div @@ -80,4 +80,3 @@ %span.icon.unarchive .dropdown-description %h4= t('administrateurs.dropdown_actions.restore') - diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml index 03d8e741f9e..bb8b926c4bf 100644 --- a/app/views/users/dossiers/_dossier_actions.html.haml +++ b/app/views/users/dossiers/_dossier_actions.html.haml @@ -7,23 +7,20 @@ - 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-mr-1w' + + - else + = link_to t('views.users.dossiers.dossier_action.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn 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--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..ff9893ca6cd 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -1,54 +1,71 @@ - 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 - = 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) - - else - = status_badge(dossier.state) - - %td.updated-at-col.cell-link - = try_format_date(dossier.updated_at) - %td.action-col.follow-col - = render partial: 'dossier_actions', locals: { dossier: dossier } + + - dossiers.each do |dossier| + .card + .flex.justify-between + %div + %h2.card-title + = link_to(url_for_dossier(dossier), class: 'cell-link') do + = procedure_libelle(dossier.procedure) + + %p.fr-icon--sm.fr-icon-user-line + = demandeur_dossier(dossier) + + %p.fr-icon--sm.fr-icon-edit-box-line + - if dossier.depose_at.present? + %span + = t('views.users.dossiers.dossiers_list.depose_at', date: dossier.depose_at.strftime('%d/%m/%Y')) + - else + %span + = t('views.users.dossiers.dossiers_list.created_at', date: dossier.created_at.strftime('%d/%m/%Y')) + - if dossier.created_at != dossier.updated_at + = t('views.users.dossiers.dossiers_list.updated_at', date: dossier.updated_at.strftime('%d/%m/%Y %H:%M')) + - 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 + + = status_badge(dossier.state, 'fr-mb-1w') + + - if dossier.pending_correction? + %br + = pending_correction_badge(:for_user) + + - if dossier.procedure.close? + = render Dsfr::AlertComponent.new(state: :info, size: 'small', 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: 'small', extra_class_names: "fr-mb-2w") do |c| + - c.body do + %p + = t('views.users.dossiers.dossiers_list.pending_correction') + + - if dossier.transfer.present? + = render Dsfr::AlertComponent.new(state: :info, size: 'small', 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 + + + .flex.justify-end + = render partial: 'dossier_actions', locals: { dossier: dossier } = paginate dossiers, views_prefix: 'shared' + - else - if filter.filter_params.present? .blank-tab diff --git a/config/locales/en.yml b/config/locales/en.yml index b91ccadad6f..b9f54df1813 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -463,14 +463,17 @@ en: 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 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}" @@ -483,8 +486,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: Sign in on %{application_name} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d8b4bb5835e..0f2351a25ef 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -465,22 +465,26 @@ fr: 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 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}" From e1ece91bb3227895a8e6a4a2dadfe21264c38e72 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Tue, 27 Jun 2023 14:33:49 +0200 Subject: [PATCH 02/21] harmonize card admin --- .../stylesheets/admin-procedures-list.scss | 17 ---- app/assets/stylesheets/card.scss | 7 ++ .../procedures/_procedures_list.html.haml | 84 ++++++++++++------- .../users/dossiers/_dossier_actions.html.haml | 1 - .../administrateurs/procedure_cloning_spec.rb | 2 +- 5 files changed, 60 insertions(+), 51 deletions(-) delete mode 100644 app/assets/stylesheets/admin-procedures-list.scss 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/card.scss b/app/assets/stylesheets/card.scss index 5e528b45399..264f71a6f57 100644 --- a/app/assets/stylesheets/card.scss +++ b/app/assets/stylesheets/card.scss @@ -20,6 +20,13 @@ } } + .logo { + width: auto; + max-width: 50px; + height: fit-content; + margin-right: $default-spacer * 2; + } + &.feedback { max-width: 600px; margin: 30px auto; diff --git a/app/views/administrateurs/procedures/_procedures_list.html.haml b/app/views/administrateurs/procedures/_procedures_list.html.haml index ede6aa56d62..ce97e44034a 100644 --- a/app/views/administrateurs/procedures/_procedures_list.html.haml +++ b/app/views/administrateurs/procedures/_procedures_list.html.haml @@ -1,41 +1,61 @@ - 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) - = link_to commencer_url(procedure.path), commencer_url(procedure.path), class: 'fr-link fr-mb-1w' - - .admin-procedures-list-timestamps - %p N° #{procedure.id} - %p créée le #{procedure.created_at.strftime('%d/%m/%Y')} - - if procedure.published_at.present? - %p publiée le #{procedure.published_at.strftime('%d/%m/%Y')} - - - if procedure.updated_at.today? - %p modifiée à #{procedure.updated_at.strftime('%H:%M')} - - else - %p 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 archivée le #{procedure.closed_at.strftime('%d/%m/%Y')} - - elsif procedure.auto_archive_on&.future? - %p 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 + créée le #{procedure.created_at.strftime('%d/%m/%Y')} + - if procedure.published_at.present? + %span publiée le #{procedure.published_at.strftime('%d/%m/%Y')} + + - if procedure.updated_at.today? + %span modifiée à #{procedure.updated_at.strftime('%H:%M')} + - else + %span modifiée le #{procedure.updated_at.strftime('%d/%m/%Y %H:%M')} + + - if procedure.closed_at.present? + %span archivée le #{procedure.closed_at.strftime('%d/%m/%Y')} + - elsif procedure.auto_archive_on&.future? + %span sera clôturée le #{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 - %span.badge.baseline= procedure.dossiers.state_not_brouillon.visible_by_administration.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 diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml index bb8b926c4bf..e10bb7183a7 100644 --- a/app/views/users/dossiers/_dossier_actions.html.haml +++ b/app/views/users/dossiers/_dossier_actions.html.haml @@ -14,7 +14,6 @@ - else = link_to t('views.users.dossiers.dossier_action.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn 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--tertiary'}) do |menu| - menu.with_button_inner_html do - if has_edit_action diff --git a/spec/system/administrateurs/procedure_cloning_spec.rb b/spec/system/administrateurs/procedure_cloning_spec.rb index a53545f09bb..e5ca7d1c4ef 100644 --- a/spec/system/administrateurs/procedure_cloning_spec.rb +++ b/spec/system/administrateurs/procedure_cloning_spec.rb @@ -34,7 +34,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') From 064e2abeed35a1708c56a7df748f1e7f6570a959 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Tue, 27 Jun 2023 15:16:36 +0200 Subject: [PATCH 03/21] add missing translation for admin cards --- .../procedures/_procedures_list.html.haml | 27 ++++++++++++------- config/locales/en.yml | 7 +++++ config/locales/fr.yml | 6 +++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/views/administrateurs/procedures/_procedures_list.html.haml b/app/views/administrateurs/procedures/_procedures_list.html.haml index ce97e44034a..b79af04c1e7 100644 --- a/app/views/administrateurs/procedures/_procedures_list.html.haml +++ b/app/views/administrateurs/procedures/_procedures_list.html.haml @@ -13,19 +13,31 @@ = link_to commencer_url(procedure.path), commencer_url(procedure.path), class: 'fr-link fr-mb-1w' %p.fr-mt-1w.fr-mb-1w - créée le #{procedure.created_at.strftime('%d/%m/%Y')} + = t('administrateurs.procedures.created_at') + = procedure.created_at.strftime('%d/%m/%Y') + - if procedure.published_at.present? - %span publiée le #{procedure.published_at.strftime('%d/%m/%Y')} + %span + = t('administrateurs.procedures.published_at') + = procedure.published_at.strftime('%d/%m/%Y') - if procedure.updated_at.today? - %span modifiée à #{procedure.updated_at.strftime('%H:%M')} + %span + = t('administrateurs.procedures.updated_at_today') + = procedure.updated_at.strftime('%H:%M') - else - %span modifiée le #{procedure.updated_at.strftime('%d/%m/%Y %H:%M')} + %span + = t('administrateurs.procedures.updated_at') + = procedure.updated_at.strftime('%d/%m/%Y %H:%M') - if procedure.closed_at.present? - %span archivée le #{procedure.closed_at.strftime('%d/%m/%Y')} + %span + = t('administrateurs.procedures.closed_at') + = procedure.closed_at.strftime('%d/%m/%Y') - elsif procedure.auto_archive_on&.future? - %span sera clôturée le #{procedure.auto_archive_on.strftime('%d/%m/%Y')} + %span + = t('administrateurs.procedures.auto_archive_on') + = procedure.auto_archive_on.strftime('%d/%m/%Y') %div - if procedure.routing_enabled? @@ -41,16 +53,13 @@ .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]) diff --git a/config/locales/en.yml b/config/locales/en.yml index b9f54df1813..1a278921577 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -747,6 +747,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: dossiers: test_procedure: "This file is submitted on a test procedure. Any modification of the procedure by the administrator (addition of a field, publication of the procedure, etc.) will result in the removal of the file." diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0f2351a25ef..4b32f2e2259 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -823,6 +823,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" From 87fceb311acc4e80dd8e22402bdba485068775b0 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Tue, 27 Jun 2023 15:24:21 +0200 Subject: [PATCH 04/21] reduce button size on card --- app/views/users/dossiers/_dossier_actions.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml index e10bb7183a7..64b54de0950 100644 --- a/app/views/users/dossiers/_dossier_actions.html.haml +++ b/app/views/users/dossiers/_dossier_actions.html.haml @@ -9,12 +9,12 @@ - if has_actions - 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-mr-1w' + = 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--tertiary fr-mr-1w' + = 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--tertiary'}) do |menu| + = 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 - if has_edit_action = t('views.users.dossiers.dossier_action.other_actions') From 3d20ef6af94d9521aba5d3f5f9b1becb1a9d9112 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Wed, 28 Jun 2023 11:53:41 +0200 Subject: [PATCH 05/21] remove specific partial for dossier transfers and use Dossier instead od DossierTransfer --- app/assets/stylesheets/buttons.scss | 2 +- app/assets/stylesheets/card.scss | 1 - app/controllers/users/dossiers_controller.rb | 10 ++--- .../users/dossiers/_dossiers_list.html.haml | 37 +++++++++++++------ .../_transfered_dossiers_list.html.haml | 35 ------------------ app/views/users/dossiers/index.html.haml | 9 ++--- .../users/dossiers/index.html.haml_spec.rb | 2 +- 7 files changed, 36 insertions(+), 60 deletions(-) delete mode 100644 app/views/users/dossiers/_transfered_dossiers_list.html.haml diff --git a/app/assets/stylesheets/buttons.scss b/app/assets/stylesheets/buttons.scss index e4333bacdc4..a7f68060142 100644 --- a/app/assets/stylesheets/buttons.scss +++ b/app/assets/stylesheets/buttons.scss @@ -183,7 +183,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 264f71a6f57..49f968b5c0b 100644 --- a/app/assets/stylesheets/card.scss +++ b/app/assets/stylesheets/card.scss @@ -6,7 +6,6 @@ border: 1px solid $border-grey; margin-bottom: $default-spacer * 2; background: #FFFFFF; - color: $dark-grey; .card-title { color: $black; diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index ba9ec0f7011..e6df42bccaf 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -28,9 +28,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' @@ -44,7 +44,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) @@ -392,14 +392,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] diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index ff9893ca6cd..8b70af8aac4 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -7,8 +7,11 @@ .flex.justify-between %div %h2.card-title - = link_to(url_for_dossier(dossier), class: 'cell-link') do - = procedure_libelle(dossier.procedure) + - if @statut == "dossiers-transferes" + = dossier.procedure.libelle + - else + = link_to(url_for_dossier(dossier), class: 'cell-link') do + = dossier.procedure.libelle %p.fr-icon--sm.fr-icon-user-line = demandeur_dossier(dossier) @@ -52,16 +55,26 @@ = t('views.users.dossiers.dossiers_list.pending_correction') - if dossier.transfer.present? - = render Dsfr::AlertComponent.new(state: :info, size: 'small', 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 - - - .flex.justify-end - = render partial: 'dossier_actions', locals: { dossier: dossier } + - if @statut == "dossiers-transferes" + = render Dsfr::AlertComponent.new(state: :info, size: 'small', extra_class_names: "fr-mt-2w") 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: 'small', 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 @statut != "dossiers-transferes" + .flex.justify-end + = render partial: 'dossier_actions', locals: { dossier: dossier } = paginate dossiers, views_prefix: 'shared' 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..df231e128a8 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -56,11 +56,11 @@ 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" @@ -94,8 +94,7 @@ = 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 } + = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - if @statut == "dossiers-expirant" = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } diff --git a/spec/views/users/dossiers/index.html.haml_spec.rb b/spec/views/users/dossiers/index.html.haml_spec.rb index 8cb30522f3e..d72a4740016 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) From 77240f40c8778d6228712a673cdd2d35d387ff4c Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Wed, 28 Jun 2023 12:48:47 +0200 Subject: [PATCH 06/21] fix specs --- app/views/users/dossiers/_dossiers_list.html.haml | 2 +- config/locales/en.yml | 2 -- config/locales/fr.yml | 2 -- spec/system/users/invite_spec.rb | 4 ++-- spec/system/users/list_dossiers_spec.rb | 15 +++++++++------ spec/views/users/dossiers/index.html.haml_spec.rb | 8 ++++---- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index 8b70af8aac4..f85c90505d8 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -76,7 +76,7 @@ .flex.justify-end = render partial: 'dossier_actions', locals: { dossier: dossier } - = paginate dossiers, views_prefix: 'shared' + = paginate dossiers, views_prefix: 'shared' - else diff --git a/config/locales/en.yml b/config/locales/en.yml index 1a278921577..25d4840e45f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -461,9 +461,7 @@ en: dossiers: "My files" dossiers_list: caption: My files - procedure: "Procedure" n_dossier: "File n." - status: "Status" 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 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4b32f2e2259..64b4fcb08b0 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -463,9 +463,7 @@ fr: dossiers: "Mes dossiers" dossiers_list: caption: Mes dossiers - procedure: "Démarche" n_dossier: "Nº dossier" - status: "Statut" 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 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..c416103e562 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,13 +178,15 @@ 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' + scenario 'the dossier is cloned' do + expect(Dossier.count).to eq(8) + within(:css, ".card", match: :first) do + click_on 'Autres actions' click_on 'Dupliquer ce dossier' end expect(page).to have_content("Votre dossier a bien été dupliqué. Vous pouvez maintenant le vérifier, l’adapter puis le déposer.") + expect(Dossier.count).to eq(9) 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 d72a4740016..0b34cf2b40b 100644 --- a/spec/views/users/dossiers/index.html.haml_spec.rb +++ b/spec/views/users/dossiers/index.html.haml_spec.rb @@ -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 From 1edfaae354f5f335fc97be19632f9406f4459bc1 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Wed, 28 Jun 2023 18:26:08 +0200 Subject: [PATCH 07/21] remove hidden dossier partial for hidden usager tab --- .../users/dossiers/_dossiers_list.html.haml | 51 ++++++++++++------- .../dossiers/_hidden_dossiers_list.html.haml | 44 ---------------- app/views/users/dossiers/index.html.haml | 2 +- config/locales/en.yml | 2 + config/locales/fr.yml | 2 + 5 files changed, 37 insertions(+), 64 deletions(-) delete mode 100644 app/views/users/dossiers/_hidden_dossiers_list.html.haml diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index f85c90505d8..d2937be3495 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -7,36 +7,44 @@ .flex.justify-between %div %h2.card-title - - if @statut == "dossiers-transferes" - = dossier.procedure.libelle - - else + - if ["dossiers-transferes", "dossiers-supprimes-recemment"].exclude?(@statut) = link_to(url_for_dossier(dossier), class: 'cell-link') do = dossier.procedure.libelle + - else + = dossier.procedure.libelle %p.fr-icon--sm.fr-icon-user-line = demandeur_dossier(dossier) - %p.fr-icon--sm.fr-icon-edit-box-line - - if dossier.depose_at.present? - %span - = t('views.users.dossiers.dossiers_list.depose_at', date: dossier.depose_at.strftime('%d/%m/%Y')) - - else - %span - = t('views.users.dossiers.dossiers_list.created_at', date: dossier.created_at.strftime('%d/%m/%Y')) - - if dossier.created_at != dossier.updated_at - = t('views.users.dossiers.dossiers_list.updated_at', date: dossier.updated_at.strftime('%d/%m/%Y %H:%M')) - - 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(', ') - + - if dossier.hidden_by_user? + %p.fr-icon--sm.fr-icon-delete-line + = t('views.users.dossiers.dossiers_list.hidden_by_user', date: dossier.hidden_by_user_at.strftime('%d/%m/%Y')) + - 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: dossier.depose_at.strftime('%d/%m/%Y')) + - else + %span + = t('views.users.dossiers.dossiers_list.created_at', date: dossier.created_at.strftime('%d/%m/%Y')) + - if dossier.created_at != dossier.updated_at + = t('views.users.dossiers.dossiers_list.updated_at', date: dossier.updated_at.strftime('%d/%m/%Y %H:%M')) + + - 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 - = status_badge(dossier.state, 'fr-mb-1w') + - if @statut == "dossiers-supprimes-recemment" + %span.fr-badge.fr-badge--warning + = t('views.users.dossiers.dossiers_list.hidden_by_user_badge') + - else + = status_badge(dossier.state, 'fr-mb-1w') - if dossier.pending_correction? %br @@ -72,10 +80,15 @@ = link_to t('views.users.dossiers.transfers.revoke'), transfer_path(dossier.transfer), class: 'fr-link', method: :delete - - if @statut != "dossiers-transferes" + - 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 + = paginate dossiers, views_prefix: 'shared' 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/index.html.haml b/app/views/users/dossiers/index.html.haml index df231e128a8..156caba530b 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -88,7 +88,7 @@ = 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 } + = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - if @statut == "dossiers-supprimes-definitivement" = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers } diff --git a/config/locales/en.yml b/config/locales/en.yml index 25d4840e45f..5e7d7113046 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -472,6 +472,8 @@ en: created_at: Created at %{date} updated_at: updated at %{date} shared_with: File shared with + hidden_by_user: Deleted at %{date} + hidden_by_user_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}" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 64b4fcb08b0..15e31d50aee 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -474,6 +474,8 @@ fr: created_at: Créé le %{date} updated_at: modifié le %{date} shared_with: Dossier partagé avec + hidden_by_user: Supprimé le %{date} + hidden_by_user_badge: Supprimé dossier_action: edit_dossier: "Modifier le dossier" start_other_dossier: "Commencer un autre dossier vide" From 6738dcf08000eb487932d79c7a565ae209cb32cf Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Thu, 29 Jun 2023 10:45:31 +0200 Subject: [PATCH 08/21] change design from table to cards for usager for deleted dossier tab --- .../dossiers/_deleted_dossiers_list.html.haml | 51 ++++++++----------- .../users/dossiers/_dossiers_list.html.haml | 4 +- app/views/users/dossiers/index.html.haml | 19 +------ config/locales/en.yml | 9 +--- config/locales/fr.yml | 9 +--- config/locales/models/deleted_dossier/en.yml | 16 ++++++ config/locales/models/deleted_dossier/fr.yml | 4 ++ spec/system/users/transfer_dossier_spec.rb | 4 +- 8 files changed, 52 insertions(+), 64 deletions(-) create mode 100644 config/locales/models/deleted_dossier/en.yml diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml index 1bedcba59f6..e4dc08cd555 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: dossier.updated_at.strftime('%d/%m/%Y')) + = "-" + = 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--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/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index d2937be3495..5ba072fb907 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -18,7 +18,7 @@ - if dossier.hidden_by_user? %p.fr-icon--sm.fr-icon-delete-line - = t('views.users.dossiers.dossiers_list.hidden_by_user', date: dossier.hidden_by_user_at.strftime('%d/%m/%Y')) + = t('views.users.dossiers.dossiers_list.deleted', date: dossier.hidden_by_user_at.strftime('%d/%m/%Y')) - else %p.fr-icon--sm.fr-icon-edit-box-line - if dossier.depose_at.present? @@ -42,7 +42,7 @@ - if @statut == "dossiers-supprimes-recemment" %span.fr-badge.fr-badge--warning - = t('views.users.dossiers.dossiers_list.hidden_by_user_badge') + = t('views.users.dossiers.dossiers_list.deleted_badge') - else = status_badge(dossier.state, 'fr-mb-1w') diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml index 156caba530b..363bdd5d4f8 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -78,23 +78,8 @@ - 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: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - - 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 } - - - if @statut == "dossiers-transferes" - = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } - - - if @statut == "dossiers-expirant" + - else = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } diff --git a/config/locales/en.yml b/config/locales/en.yml index 5e7d7113046..d41dec480ed 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -393,10 +393,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" archived_dossier: "Your file will be kept %{duree_conservation_dossiers_dans_ds} more months" identite: @@ -460,7 +456,6 @@ en: index: dossiers: "My files" dossiers_list: - caption: My files n_dossier: "File n." 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." @@ -472,8 +467,8 @@ en: created_at: Created at %{date} updated_at: updated at %{date} shared_with: File shared with - hidden_by_user: Deleted at %{date} - hidden_by_user_badge: Deleted + 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}" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 15e31d50aee..c10753a6339 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -395,10 +395,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" archived_dossier: "Votre dossier sera conservé %{duree_conservation_dossiers_dans_ds} mois supplémentaire" identite: @@ -462,7 +458,6 @@ fr: index: dossiers: "Mes dossiers" dossiers_list: - caption: Mes dossiers n_dossier: "Nº dossier" 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." @@ -474,8 +469,8 @@ fr: created_at: Créé le %{date} updated_at: modifié le %{date} shared_with: Dossier partagé avec - hidden_by_user: Supprimé le %{date} - hidden_by_user_badge: Supprimé + deleted: Supprimé le %{date} + deleted_badge: Supprimé dossier_action: edit_dossier: "Modifier le dossier" start_other_dossier: "Commencer un autre dossier vide" 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/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 From e03696c380f7ccf376a572c7ac977f5eb4e936dd Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Thu, 29 Jun 2023 12:43:05 +0200 Subject: [PATCH 09/21] translate dates and remove demandeur icon if not present --- .../users/dossiers/_deleted_dossiers_list.html.haml | 2 +- app/views/users/dossiers/_dossiers_list.html.haml | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml index e4dc08cd555..b38f6468e65 100644 --- a/app/views/users/dossiers/_deleted_dossiers_list.html.haml +++ b/app/views/users/dossiers/_deleted_dossiers_list.html.haml @@ -10,7 +10,7 @@ = dossier.procedure.libelle %p.fr-icon--sm.fr-icon-delete-line.fr-mb-0 - = t('views.users.dossiers.dossiers_list.deleted', date: dossier.updated_at.strftime('%d/%m/%Y')) + = t('views.users.dossiers.dossiers_list.deleted', date: l(dossier.updated_at.to_date)) = "-" = t("activerecord.attributes.deleted_dossier.reason.#{dossier.reason}") diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index 5ba072fb907..e848619e8cb 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -13,22 +13,23 @@ - else = dossier.procedure.libelle - %p.fr-icon--sm.fr-icon-user-line - = demandeur_dossier(dossier) + - 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: dossier.hidden_by_user_at.strftime('%d/%m/%Y')) + = 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: dossier.depose_at.strftime('%d/%m/%Y')) + = t('views.users.dossiers.dossiers_list.depose_at', date: l(dossier.depose_at.to_date)) - else %span - = t('views.users.dossiers.dossiers_list.created_at', date: dossier.created_at.strftime('%d/%m/%Y')) + = 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: dossier.updated_at.strftime('%d/%m/%Y %H:%M')) + = 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 From 0a0665bb4d56e247819017d55ff982334deba745 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Thu, 29 Jun 2023 13:53:23 +0200 Subject: [PATCH 10/21] feedback PR Colin --- app/components/dsfr/alert_component.rb | 10 +++++----- .../dsfr/alert_component/alert_component.html.haml | 2 +- app/views/users/dossiers/_dossiers_list.html.haml | 8 ++++---- spec/system/users/list_dossiers_spec.rb | 4 +--- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/components/dsfr/alert_component.rb b/app/components/dsfr/alert_component.rb index 42ed9f432fc..7323f639b91 100644 --- a/app/components/dsfr/alert_component.rb +++ b/app/components/dsfr/alert_component.rb @@ -13,11 +13,11 @@ def prefix_for_state end def alert_class(state) - if size == 'small' - ["fr-alert fr-alert--sm fr-alert--#{state}", extra_class_names].compact.flatten - else - ["fr-alert fr-alert--#{state}", extra_class_names].compact.flatten - end + class_names( + "fr-alert fr-alert--#{state}" => true, + "fr-alert--sm" => size == :sm, + extra_class_names => true + ) end private diff --git a/app/components/dsfr/alert_component/alert_component.html.haml b/app/components/dsfr/alert_component/alert_component.html.haml index a0f9cf28d85..744d56529b9 100644 --- a/app/components/dsfr/alert_component/alert_component.html.haml +++ b/app/components/dsfr/alert_component/alert_component.html.haml @@ -1,5 +1,5 @@ %div{ class: alert_class(state) } - - if size != 'small' + - if size != :sm = content_tag(heading_level, class: 'fr-alert__title') do = "#{prefix_for_state}#{title}" = body diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index e848619e8cb..023e87a507d 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -52,20 +52,20 @@ = pending_correction_badge(:for_user) - if dossier.procedure.close? - = render Dsfr::AlertComponent.new(state: :info, size: 'small', extra_class_names: "fr-mb-2w") do |c| + = 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: 'small', extra_class_names: "fr-mb-2w") do |c| + = 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: 'small', extra_class_names: "fr-mt-2w") do |c| + = 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) @@ -73,7 +73,7 @@ = 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: 'small', extra_class_names: "fr-mb-2w") do |c| + = 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) diff --git a/spec/system/users/list_dossiers_spec.rb b/spec/system/users/list_dossiers_spec.rb index c416103e562..a0d4e4e7ec5 100644 --- a/spec/system/users/list_dossiers_spec.rb +++ b/spec/system/users/list_dossiers_spec.rb @@ -179,14 +179,12 @@ context 'when user clicks on clone button', js: true do scenario 'the dossier is cloned' do - expect(Dossier.count).to eq(8) within(:css, ".card", match: :first) do click_on 'Autres actions' - click_on 'Dupliquer ce dossier' + 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.") - expect(Dossier.count).to eq(9) end end end From b43889146fcd6f6681f4260fe3cee1abda94d146 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Thu, 6 Jul 2023 17:36:28 +0200 Subject: [PATCH 11/21] feebacks PR Colin part 2 --- app/assets/stylesheets/card.scss | 2 +- .../dossiers/_deleted_dossiers_list.html.haml | 2 +- .../users/dossiers/_dossiers_list.html.haml | 4 +-- app/views/users/dossiers/index.html.haml | 36 ++++++++++--------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/assets/stylesheets/card.scss b/app/assets/stylesheets/card.scss index 49f968b5c0b..8f8bd538ba6 100644 --- a/app/assets/stylesheets/card.scss +++ b/app/assets/stylesheets/card.scss @@ -4,7 +4,7 @@ .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; .card-title { diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml index b38f6468e65..853a0c4ea35 100644 --- a/app/views/users/dossiers/_deleted_dossiers_list.html.haml +++ b/app/views/users/dossiers/_deleted_dossiers_list.html.haml @@ -19,7 +19,7 @@ = t('views.users.dossiers.dossiers_list.n_dossier') = dossier.dossier_id - %span.fr-badge.fr-badge--warning + %span.fr-badge.fr-badge--sm.fr-badge--warning = t('views.users.dossiers.dossiers_list.deleted_badge') = paginate deleted_dossiers, views_prefix: 'shared' diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index 023e87a507d..566c2dbd35e 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -42,7 +42,7 @@ = dossier.id - if @statut == "dossiers-supprimes-recemment" - %span.fr-badge.fr-badge--warning + %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') @@ -51,7 +51,7 @@ %br = pending_correction_badge(:for_user) - - if dossier.procedure.close? + - 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 diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml index 363bdd5d4f8..0517f8a9e42 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -63,23 +63,25 @@ 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' + .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 } + - 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) + - 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 } + - 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 } From db706243980cf0cf717337dba716a345f5253457 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 10 Jul 2023 10:51:06 +0200 Subject: [PATCH 12/21] feat(pj): change naming scheme to %Y/%m/%d/sd/sd..... --- app/jobs/pjs_migration_job.rb | 2 +- config/initializers/active_storage.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From b41a3586ff69b741d5bc4460f8091e4887d5b6d5 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Mon, 10 Jul 2023 14:55:20 +0200 Subject: [PATCH 13/21] fix specs --- .../routing/rules_full_scenario_spec.rb | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/system/routing/rules_full_scenario_spec.rb b/spec/system/routing/rules_full_scenario_spec.rb index e349def0a39..50280fef702 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) From 52545dc86c4a01519f44df654c00e21fb98efea4 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 7 Jul 2023 10:38:30 +0200 Subject: [PATCH 14/21] fix(type_de_champ): feature flag support --- app/components/types_de_champ_editor/champ_component.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/types_de_champ_editor/champ_component.rb b/app/components/types_de_champ_editor/champ_component.rb index 2ee7c9e6ae4..6fe4fb9a1da 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? || procedure.feature_enabled?(feature_name) end def filter_type_champ(type_champ) From a9f431caa531787f3a9f22a38d2473cc0fc3fd69 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 30 May 2023 11:39:53 +0200 Subject: [PATCH 15/21] feat(dossier): implement champ polling --- .../editable_champ_component.rb | 22 ++++++++++++++++++- .../instructeurs/dossiers_controller.rb | 14 ++++++++++++ app/controllers/users/dossiers_controller.rb | 20 ++++++++++++++--- app/models/champ.rb | 10 +++++++++ config/routes.rb | 2 ++ 5 files changed, 64 insertions(+), 4 deletions(-) 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/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 64a10cc6e41..8f27770705e 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -302,6 +302,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 ba9ec0f7011..3ed0eac18f0 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -6,11 +6,11 @@ class DossiersController < UserController layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret] ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all] - ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update, :create_commentaire, :papertrail, :restore] + 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] 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] @@ -274,6 +274,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) @@ -465,7 +479,7 @@ def champs_public_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/models/champ.rb b/app/models/champ.rb index 00211b1daa6..ca6c67b1ebc 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -223,6 +223,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/config/routes.rb b/config/routes.rb index 1ff48ea0b3a..0918cb8aba7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -322,6 +322,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' @@ -434,6 +435,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' From c6d728d44fdc5ff68ee78f35bbd0771454364dc7 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 30 May 2023 11:41:51 +0200 Subject: [PATCH 16/21] feat(api): HTTP API client --- Gemfile | 1 + Gemfile.lock | 8 ++++ app/lib/api/client.rb | 97 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 app/lib/api/client.rb diff --git a/Gemfile b/Gemfile index 520c043548b..61489a29f01 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,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 6d5caab37a2..4536d791af0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -218,7 +218,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) @@ -828,6 +835,7 @@ DEPENDENCIES devise-two-factor discard dotenv-rails + dry-monads elastic-apm factory_bot flipper 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 From c74351459ea57c0f12010458230e682f06f7c728 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 30 May 2023 11:43:56 +0200 Subject: [PATCH 17/21] feat(champ): add result error handling to ChampFetchExternalDataJob --- app/jobs/champ_fetch_external_data_job.rb | 23 ++++++++++-- .../champ_fetch_external_data_job_spec.rb | 35 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) 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/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 From d5820277c059fca498c144e191e882ee9ac5308e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 30 May 2023 14:42:36 +0200 Subject: [PATCH 18/21] feat(cojo): add cojo type de champ --- .../champs_rows_show_component.html.haml | 2 + .../editable_champ/cojo_component.rb | 9 +++ .../cojo_component/cojo_component.en.yml | 7 ++ .../cojo_component/cojo_component.fr.yml | 7 ++ .../cojo_component/cojo_component.html.haml | 23 +++++++ app/controllers/users/dossiers_controller.rb | 19 +++++- app/graphql/api/v2/schema.rb | 1 + app/graphql/schema.graphql | 33 ++++++++++ app/graphql/types/champ_descriptor_type.rb | 2 + .../descriptor/cojo_champ_descriptor_type.rb | 5 ++ .../controllers/format_controller.ts | 10 +++ app/models/champs/cojo_champ.rb | 64 +++++++++++++++++++ app/models/type_de_champ.rb | 13 ++-- .../types_de_champ/cojo_type_de_champ.rb | 2 + app/schemas/accreditation-cojo.json | 14 ++++ app/services/cojo_service.rb | 56 ++++++++++++++++ app/views/shared/champs/cojo/_show.html.haml | 9 +++ config/env.example.optional | 6 ++ config/initializers/02_urls.rb | 1 + config/initializers/flipper.rb | 3 +- config/initializers/inflections.rb | 1 + config/locales/models/type_de_champ/en.yml | 1 + config/locales/models/type_de_champ/fr.yml | 1 + spec/factories/champ.rb | 4 ++ spec/factories/type_de_champ.rb | 3 + .../files/api_cojo/accreditation_invalid.json | 4 ++ .../files/api_cojo/accreditation_no.json | 5 ++ .../files/api_cojo/accreditation_yes.json | 5 ++ ...0220705164551_remove_unused_champs_spec.rb | 4 +- spec/models/champs/cojo_champ_spec.rb | 50 +++++++++++++++ .../services/procedure_export_service_spec.rb | 9 ++- 31 files changed, 361 insertions(+), 12 deletions(-) create mode 100644 app/components/editable_champ/cojo_component.rb create mode 100644 app/components/editable_champ/cojo_component/cojo_component.en.yml create mode 100644 app/components/editable_champ/cojo_component/cojo_component.fr.yml create mode 100644 app/components/editable_champ/cojo_component/cojo_component.html.haml create mode 100644 app/graphql/types/champs/descriptor/cojo_champ_descriptor_type.rb create mode 100644 app/models/champs/cojo_champ.rb create mode 100644 app/models/types_de_champ/cojo_type_de_champ.rb create mode 100644 app/schemas/accreditation-cojo.json create mode 100644 app/services/cojo_service.rb create mode 100644 app/views/shared/champs/cojo/_show.html.haml create mode 100644 spec/fixtures/files/api_cojo/accreditation_invalid.json create mode 100644 spec/fixtures/files/api_cojo/accreditation_no.json create mode 100644 spec/fixtures/files/api_cojo/accreditation_yes.json create mode 100644 spec/models/champs/cojo_champ_spec.rb 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/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/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 3ed0eac18f0..dcc0885266b 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -471,8 +471,23 @@ 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, :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, + :code_departement, + :accreditation_number, + :accreditation_birthdate, + value: [] ]) champs_params[:champs_public_all_attributes] = champs_params.delete(:champs_public_attributes) || {} champs_params diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb index 8ff3cdfb150..89372aa1593 100644 --- a/app/graphql/api/v2/schema.rb +++ b/app/graphql/api/v2/schema.rb @@ -84,6 +84,7 @@ def self.resolve_type(type_definition, object, ctx) Types::Champs::Descriptor::CheckboxChampDescriptorType, Types::Champs::Descriptor::CiviliteChampDescriptorType, Types::Champs::Descriptor::CnafChampDescriptorType, + Types::Champs::Descriptor::COJOChampDescriptorType, Types::Champs::Descriptor::CommuneChampDescriptorType, Types::Champs::Descriptor::DateChampDescriptorType, Types::Champs::Descriptor::DatetimeChampDescriptorType, diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 10548210bed..8a5f619ff63 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! @@ -3699,6 +3727,11 @@ enum TypeDeChamp { """ cnaf + """ + Accréditation Paris 2024 + """ + cojo + """ Communes """ diff --git a/app/graphql/types/champ_descriptor_type.rb b/app/graphql/types/champ_descriptor_type.rb index 05fa6058ad4..3e59bec9694 100644 --- a/app/graphql/types/champ_descriptor_type.rb +++ b/app/graphql/types/champ_descriptor_type.rb @@ -94,6 +94,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/models/champs/cojo_champ.rb b/app/models/champs/cojo_champ.rb new file mode 100644 index 00000000000..05e50836d01 --- /dev/null +++ b/app/models/champs/cojo_champ.rb @@ -0,0 +1,64 @@ +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 90cec27e038..ee211fb5b8c 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -18,7 +18,9 @@ class TypeDeChamp < ApplicationRecord self.ignored_columns += [:migrated_parent, :revision_id, :parent_id, :order_place] FILE_MAX_SIZE = 200.megabytes - FEATURE_FLAGS = {} + FEATURE_FLAGS = { + cojo: :cojo_type_de_champ + } MINIMUM_TEXTAREA_CHARACTER_LIMIT_LENGTH = 400 STRUCTURE = :structure @@ -68,7 +70,8 @@ class TypeDeChamp < ApplicationRecord cnaf: REFERENTIEL_EXTERNE, dgfip: REFERENTIEL_EXTERNE, pole_emploi: REFERENTIEL_EXTERNE, - mesri: REFERENTIEL_EXTERNE + mesri: REFERENTIEL_EXTERNE, + cojo: REFERENTIEL_EXTERNE } enum type_champs: { @@ -107,7 +110,8 @@ class TypeDeChamp < ApplicationRecord dgfip: 'dgfip', pole_emploi: 'pole_emploi', mesri: 'mesri', - epci: 'epci' + epci: 'epci', + cojo: 'cojo' } store_accessor :options, @@ -569,7 +573,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/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/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 cc69ed22a50..4c7b1a256ef 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/flipper.rb b/config/initializers/flipper.rb index c0aec97b958..84ed2984ccb 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -18,7 +18,8 @@ def setup_features(features) :hide_instructeur_email, :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/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 989cba47324..ff0d3ab06e6 100644 --- a/config/locales/models/type_de_champ/fr.yml +++ b/config/locales/models/type_de_champ/fr.yml @@ -50,6 +50,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/spec/factories/champ.rb b/spec/factories/champ.rb index 350f2814ab9..b3a15774d10 100644 --- a/spec/factories/champ.rb +++ b/spec/factories/champ.rb @@ -239,6 +239,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 bb9587a37ca..cea0c942c2f 100644 --- a/spec/factories/type_de_champ.rb +++ b/spec/factories/type_de_champ.rb @@ -181,6 +181,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) } 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/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb index a5c277cb6dc..8056bbbec44 100644 --- a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb +++ b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb @@ -14,9 +14,9 @@ describe 'remove_unused_champs' do it "with bad champs" do - expect(Champ.where(dossier: dossier).count).to eq(40) + expect(Champ.where(dossier: dossier).count).to eq(41) run_task - expect(Champ.where(dossier: dossier).count).to eq(39) + expect(Champ.where(dossier: dossier).count).to eq(40) end end end 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/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index 4e2a729564f..fc2d3dd27c9 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -88,7 +88,8 @@ "text", "epci", "epci (Code)", - "epci (Département)" + "epci (Département)", + "cojo" ] end @@ -198,7 +199,8 @@ "text", "epci", "epci (Code)", - "epci (Département)" + "epci (Département)", + "cojo" ] end @@ -291,7 +293,8 @@ "text", "epci", "epci (Code)", - "epci (Département)" + "epci (Département)", + "cojo" ] end From e47761860a1bc8775720965e36cb540f8e70f752 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Mon, 10 Jul 2023 15:41:50 +0200 Subject: [PATCH 19/21] fix(demande): don't crash when siret siege social is missing --- .../dossiers/_identite_entreprise.html.haml | 2 +- .../_identite_entreprise.html.haml_spec.rb | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/views/shared/dossiers/_identite_entreprise.html.haml b/app/views/shared/dossiers/_identite_entreprise.html.haml index bdc7a60f8f3..30824f88861 100644 --- a/app/views/shared/dossiers/_identite_entreprise.html.haml +++ b/app/views/shared/dossiers/_identite_entreprise.html.haml @@ -28,7 +28,7 @@ - unless local_assigns[:short_identity] - - if etablissement.siret != etablissement.entreprise.siret_siege_social + - if etablissement.entreprise.siret_siege_social.present? && etablissement.siret != etablissement.entreprise.siret_siege_social = render Dossiers::RowShowComponent.new(label: "SIRET du siège social") do |c| - c.with_value do %p 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..78bcfcb0c5c 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("SIRET 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("SIRET du siège social") + end + end + end end end From a6fb762772c1f1b0de6e07560cdf0eadd3c6babd Mon Sep 17 00:00:00 2001 From: Christian Lautier <15379878+maatinito@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:15:03 -1000 Subject: [PATCH 20/21] Merge branch 'feature/cojo_field' --- Gemfile | 1 + Gemfile.lock | 8 ++ .../champs_rows_show_component.html.haml | 2 + .../editable_champ/cojo_component.rb | 9 ++ .../cojo_component/cojo_component.en.yml | 7 + .../cojo_component/cojo_component.fr.yml | 7 + .../cojo_component/cojo_component.html.haml | 23 +++ .../editable_champ_component.rb | 22 ++- .../types_de_champ_editor/champ_component.rb | 4 +- .../instructeurs/dossiers_controller.rb | 14 ++ app/controllers/users/dossiers_controller.rb | 41 +++++- app/graphql/api/v2/schema.rb | 1 + app/graphql/schema.graphql | 33 +++++ app/graphql/schema.json | 136 ++++++++++++++++++ app/graphql/types/champ_descriptor_type.rb | 2 + .../descriptor/cojo_champ_descriptor_type.rb | 5 + .../controllers/format_controller.ts | 10 ++ app/jobs/champ_fetch_external_data_job.rb | 23 ++- app/lib/api/client.rb | 97 +++++++++++++ app/models/champ.rb | 10 ++ app/models/champs/cojo_champ.rb | 86 +++++++++++ app/models/type_de_champ.rb | 15 +- .../types_de_champ/cojo_type_de_champ.rb | 2 + app/schemas/accreditation-cojo.json | 14 ++ app/services/cojo_service.rb | 56 ++++++++ app/views/shared/champs/cojo/_show.html.haml | 9 ++ config/env.example.optional | 6 + config/initializers/02_urls.rb | 1 + config/initializers/flipper.rb | 3 +- config/initializers/inflections.rb | 1 + config/locales/models/type_de_champ/en.yml | 1 + config/locales/models/type_de_champ/fr.yml | 1 + config/routes.rb | 2 + spec/factories/champ.rb | 4 + spec/factories/type_de_champ.rb | 3 + .../files/api_cojo/accreditation_invalid.json | 4 + .../files/api_cojo/accreditation_no.json | 5 + .../files/api_cojo/accreditation_yes.json | 5 + .../champ_fetch_external_data_job_spec.rb | 35 ++++- ...0220705164551_remove_unused_champs_spec.rb | 2 +- spec/models/champs/cojo_champ_spec.rb | 50 +++++++ 41 files changed, 741 insertions(+), 19 deletions(-) create mode 100644 app/components/editable_champ/cojo_component.rb create mode 100644 app/components/editable_champ/cojo_component/cojo_component.en.yml create mode 100644 app/components/editable_champ/cojo_component/cojo_component.fr.yml create mode 100644 app/components/editable_champ/cojo_component/cojo_component.html.haml create mode 100644 app/graphql/types/champs/descriptor/cojo_champ_descriptor_type.rb create mode 100644 app/lib/api/client.rb create mode 100644 app/models/champs/cojo_champ.rb create mode 100644 app/models/types_de_champ/cojo_type_de_champ.rb create mode 100644 app/schemas/accreditation-cojo.json create mode 100644 app/services/cojo_service.rb create mode 100644 app/views/shared/champs/cojo/_show.html.haml create mode 100644 spec/fixtures/files/api_cojo/accreditation_invalid.json create mode 100644 spec/fixtures/files/api_cojo/accreditation_no.json create mode 100644 spec/fixtures/files/api_cojo/accreditation_yes.json create mode 100644 spec/models/champs/cojo_champ_spec.rb 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/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/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..81191280669 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] @@ -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) @@ -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/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/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/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/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/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/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 From eba5de4becf8e31db52321873feda94767dff972 Mon Sep 17 00:00:00 2001 From: Christian Lautier <15379878+maatinito@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:04:37 -1000 Subject: [PATCH 21/21] =?UTF-8?q?fix=20SIRET=20renamings=20to=20Num=C3=A9r?= =?UTF-8?q?o=20TAHITI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/shared/dossiers/_identite_entreprise.html.haml | 2 +- spec/controllers/champs/siret_controller_spec.rb | 2 +- .../shared/dossiers/_identite_entreprise.html.haml_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/dossiers/_identite_entreprise.html.haml b/app/views/shared/dossiers/_identite_entreprise.html.haml index 93e815f6eab..79845792db7 100644 --- a/app/views/shared/dossiers/_identite_entreprise.html.haml +++ b/app/views/shared/dossiers/_identite_entreprise.html.haml @@ -29,7 +29,7 @@ - unless local_assigns[:short_identity] - if etablissement.entreprise.siret_siege_social.present? && etablissement.siret != etablissement.entreprise.siret_siege_social - = render Dossiers::RowShowComponent.new(label: "Numero TAHITI du siège social") do |c| + = 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/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/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb b/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb index 78bcfcb0c5c..84a8e735e27 100644 --- a/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb +++ b/spec/views/shared/dossiers/_identite_entreprise.html.haml_spec.rb @@ -73,7 +73,7 @@ it 'shows both sirets' do expect(subject).to include("123 456 789 00001") - expect(subject).to include("SIRET du siège social") + expect(subject).to include("Numéro TAHITI du siège social") expect(subject).to include("987 654 321 00001") end end @@ -82,7 +82,7 @@ let(:siret_siege_social) { nil } it 'does not duplicate siret' do - expect(subject).not_to include("SIRET du siège social") + expect(subject).not_to include("Numéro TAHITI du siège social") end end end