From bf8c218d3ab96aa835df1323f36aa1a97483008c Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Wed, 12 Apr 2023 16:58:15 +0200 Subject: [PATCH 01/75] Disabled select_all checkbox if all checkboxes are disabled --- .../controllers/batch_operation_controller.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/javascript/controllers/batch_operation_controller.ts b/app/javascript/controllers/batch_operation_controller.ts index abc294b9c..96bc0651d 100644 --- a/app/javascript/controllers/batch_operation_controller.ts +++ b/app/javascript/controllers/batch_operation_controller.ts @@ -149,3 +149,22 @@ function emptyCheckboxes() { ); inputs.forEach((e) => (e.checked = false)); } + +// Disable select_all checkbox if all checkboxes are disabled +addEventListener('DOMContentLoaded', () => { + const checkbox_all = document.querySelector( + '#checkbox_all_batch_operation' + ); + const inputs = document.querySelectorAll( + 'div[data-controller="batch-operation"] input[data-batch-operation-target="input"]' + ); + if (checkbox_all) { + const disabled_inputs = Array.from(inputs).filter( + (input) => input.disabled == true + ); + if (disabled_inputs.length == inputs.length) { + checkbox_all.checked = true; + checkbox_all.disabled = true; + } + } +}); From f76232301782dddcabbb42eb3903446c25894db9 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Fri, 14 Apr 2023 16:24:50 +0200 Subject: [PATCH 02/75] change searchbar from header to dashboard for user --- app/views/layouts/_header.haml | 9 ++-- .../layouts/_search_dossiers_form.html.haml | 2 +- app/views/users/dossiers/index.html.haml | 47 +++++++++++-------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/app/views/layouts/_header.haml b/app/views/layouts/_header.haml index c447fad9e..f00aca282 100644 --- a/app/views/layouts/_header.haml +++ b/app/views/layouts/_header.haml @@ -60,16 +60,13 @@ - if params[:controller] == 'recherche' - = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_index_path } + = render partial: 'layouts/search_dossiers_form' - if is_instructeur_context - = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_index_path } + = render partial: 'layouts/search_dossiers_form' - if is_expert_context - = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_index_path } - - - if is_user_context && current_user.dossiers.count > 2 - = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_dossiers_path } + = render partial: 'layouts/search_dossiers_form' - has_header = [is_instructeur_context, is_expert_context, is_user_context] #burger-menu.fr-header__menu.fr-modal diff --git a/app/views/layouts/_search_dossiers_form.html.haml b/app/views/layouts/_search_dossiers_form.html.haml index 9d0a0a7ff..f0aab616e 100644 --- a/app/views/layouts/_search_dossiers_form.html.haml +++ b/app/views/layouts/_search_dossiers_form.html.haml @@ -2,7 +2,7 @@ .fr-container.fr-container-lg--fluid %button.fr-btn--close.fr-btn{ "aria-controls" => "search-modal", :title => t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header]) #search-473.fr-search-bar.fr-search-bar--lg - = form_tag "#{search_endpoint}", method: :get, :role => "search", class: "flex width-100" do + = form_tag recherche_index_path, method: :get, :role => "search", class: "flex width-100" do = label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label' = text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input" %button.fr-btn diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml index d5db5b69c..f18d86231 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -8,12 +8,16 @@ .dossiers-headers.sub-header .container - - if @search_terms.present? - %h1.page-title Résultat de la recherche pour « #{@search_terms} » - = render partial: "dossiers_list", locals: { dossiers: @dossiers } + %h1.page-title= t('views.users.dossiers.index.dossiers') + - if current_user.dossiers.count > 2 + #search-2.fr-search-bar.fr-search-bar--lg{ role: "search", "aria-label": t('views.users.dossiers.search.search_file') } + = form_tag recherche_dossiers_path, method: :get, :role => "search", class: "flex width-100 fr-mb-5w" do + = label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label' + = text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input" + %button.fr-btn + = t('views.users.dossiers.search.simple') - - else - %h1.page-title= t('views.users.dossiers.index.dossiers') + - if @search_terms.blank? %nav.tabs{ role: 'navigation', 'aria-label': t('views.users.dossiers.secondary_menu') } %ul - if @user_dossiers.present? @@ -59,23 +63,28 @@ badge: number_with_html_delimiter(@dossier_transfers.count)) .container - - if @statut == "en-cours" - = render partial: "dossiers_list", locals: { dossiers: @user_dossiers } + - if @search_terms.present? + %h2.page-title Résultat de la recherche pour « #{@search_terms} » + = render partial: "dossiers_list", locals: { dossiers: @dossiers } - - if @statut == "traites" - = render partial: "dossiers_list", locals: { dossiers: @dossiers_traites } + - else + - if @statut == "en-cours" + = render partial: "dossiers_list", locals: { dossiers: @user_dossiers } - - if @statut == "dossiers-invites" - = render partial: "dossiers_list", locals: { dossiers: @dossiers_invites } + - if @statut == "traites" + = render partial: "dossiers_list", locals: { dossiers: @dossiers_traites } - - if @statut == "dossiers-supprimes-recemment" - = render partial: "hidden_dossiers_list", locals: { hidden_dossiers: @dossiers_supprimes_recemment } + - if @statut == "dossiers-invites" + = render partial: "dossiers_list", locals: { dossiers: @dossiers_invites } - - if @statut == "dossiers-supprimes-definitivement" - = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes_definitivement } + - if @statut == "dossiers-supprimes-recemment" + = render partial: "hidden_dossiers_list", locals: { hidden_dossiers: @dossiers_supprimes_recemment } - - if @statut == "dossiers-transferes" - = render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossier_transfers } + - if @statut == "dossiers-supprimes-definitivement" + = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes_definitivement } - - if @statut == "dossiers-expirant" - = render partial: "dossiers_list", locals: { dossiers: @dossiers_close_to_expiration } + - if @statut == "dossiers-transferes" + = render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossier_transfers } + + - if @statut == "dossiers-expirant" + = render partial: "dossiers_list", locals: { dossiers: @dossiers_close_to_expiration } From cb6875352edeb65f9bb1c1ffe07d54a0b9e67d3d Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 14 Apr 2023 17:41:22 +0200 Subject: [PATCH 03/75] =?UTF-8?q?fix(dossier):=20d=C3=A9sactiver=20la=20ch?= =?UTF-8?q?eckbox=20select=5Fall=20quand=20action=20en=20cours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instructeurs/procedures_controller.rb | 1 + .../controllers/batch_operation_controller.ts | 19 ------------------- .../instructeurs/procedures/show.html.haml | 2 +- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 2ea5f4650..5e497ddc6 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -89,6 +89,7 @@ module Instructeurs .per(ITEMS_PER_PAGE) @projected_dossiers = DossierProjectionService.project(@filtered_sorted_paginated_ids, procedure_presentation.displayed_fields) + @disable_checkbox_all = @projected_dossiers.all? { _1.batch_operation_id.present? } assign_exports @batch_operations = BatchOperation.joins(:groupe_instructeurs) diff --git a/app/javascript/controllers/batch_operation_controller.ts b/app/javascript/controllers/batch_operation_controller.ts index 96bc0651d..abc294b9c 100644 --- a/app/javascript/controllers/batch_operation_controller.ts +++ b/app/javascript/controllers/batch_operation_controller.ts @@ -149,22 +149,3 @@ function emptyCheckboxes() { ); inputs.forEach((e) => (e.checked = false)); } - -// Disable select_all checkbox if all checkboxes are disabled -addEventListener('DOMContentLoaded', () => { - const checkbox_all = document.querySelector( - '#checkbox_all_batch_operation' - ); - const inputs = document.querySelectorAll( - 'div[data-controller="batch-operation"] input[data-batch-operation-target="input"]' - ); - if (checkbox_all) { - const disabled_inputs = Array.from(inputs).filter( - (input) => input.disabled == true - ); - if (disabled_inputs.length == inputs.length) { - checkbox_all.checked = true; - checkbox_all.disabled = true; - } - } -}); diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 52a0d0dc4..9796192d1 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -109,7 +109,7 @@ %tr - if batch_operation_component.render? %th.text-center - %input{ type: "checkbox", data: { action: "batch-operation#onCheckAll" }, id: dom_id(BatchOperation.new, :checkbox_all), aria: { label: t('views.instructeurs.dossiers.select_all') } } + %input{ type: "checkbox", disabled: @disable_checkbox_all, checked: @disable_checkbox_all, data: { action: "batch-operation#onCheckAll" }, id: dom_id(BatchOperation.new, :checkbox_all), aria: { label: t('views.instructeurs.dossiers.select_all') } } - else - if @statut.in? %w(suivis traites tous) = render partial: "header_field", locals: { field: { "label" => "●", "table" => "notifications", "column" => "notifications" }, classname: "notification-col text-center" } From 4ed29268bf2dc1e52dbbc1611ba086e6589f5ab1 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Fri, 14 Apr 2023 17:42:37 +0200 Subject: [PATCH 04/75] fix table display for search result --- app/views/recherche/index.html.haml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/views/recherche/index.html.haml b/app/views/recherche/index.html.haml index ed16059ff..6a0785a95 100644 --- a/app/views/recherche/index.html.haml +++ b/app/views/recherche/index.html.haml @@ -12,16 +12,17 @@ - if @projected_dossiers.present? = paginate @paginated_ids - %table.table.dossiers-table.hoverable - %thead - %tr - %th.notification-col - %th.number-col Nº dossier - %th Démarche - %th Demandeur - %th.status-col Statut - %th.action-col.follow-col - %tbody + .fr-table.fr-table--bordered + %table.table.dossiers-table.hoverable + %thead + %tr + %th.notification-col + %th.number-col Nº dossier + %th Démarche + %th Demandeur + %th.status-col Statut + %th.action-col.follow-col + %tbody - @projected_dossiers.each do |p| - procedure_libelle, user_email, procedure_id = p.columns - instructeur_dossier = @instructeur_dossiers_ids.include?(p.dossier_id) @@ -90,7 +91,9 @@ = t('views.instructeurs.dossiers.restore') - else - %td.action-col.follow-col= render partial: "instructeurs/procedures/dossier_actions", + %td.action-col.follow-col + %ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right + = render partial: "instructeurs/procedures/dossier_actions", locals: { procedure_id: procedure_id, dossier_id: p.dossier_id, state: p.state, From e140d1cc7a7e642764aa9336c080aafb268e8f96 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Fri, 14 Apr 2023 17:43:38 +0200 Subject: [PATCH 05/75] restore the hidden overflow from DSFR on table --- app/assets/stylesheets/procedure_show.scss | 1 - app/views/recherche/index.html.haml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/procedure_show.scss b/app/assets/stylesheets/procedure_show.scss index 9d3466d88..027934ea5 100644 --- a/app/assets/stylesheets/procedure_show.scss +++ b/app/assets/stylesheets/procedure_show.scss @@ -22,7 +22,6 @@ .dossiers-table { margin-top: $default-spacer; margin-bottom: 3 * $default-spacer; - overflow: visible; // remove DSFR hidden overflow because of dropdown } .procedure-actions { diff --git a/app/views/recherche/index.html.haml b/app/views/recherche/index.html.haml index 6a0785a95..2057de441 100644 --- a/app/views/recherche/index.html.haml +++ b/app/views/recherche/index.html.haml @@ -1,6 +1,6 @@ - content_for(:title, "Recherche : #{@search_terms}") -.container +.fr-container - if @dossier_not_in_instructor_group.present? .fr-alert.fr-alert--info.fr-alert--sm.fr-mt-3w From bf6cc25063bcd7b562bd2a5ceec5664f9e951669 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 11 Apr 2023 10:11:08 +0200 Subject: [PATCH 06/75] feat(graphql): expose pendingDeletedDossiers on groupe instructeur --- app/graphql/schema.graphql | 35 ++++++++++++++++++ .../groupe_instructeur_with_dossiers_type.rb | 15 ++++++++ .../graphql_controller_stored_queries_spec.rb | 36 ++++++++++++++++++- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 8bc5054a9..76d2b930f 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -2478,6 +2478,41 @@ type GroupeInstructeurWithDossiers { Le numero du groupe instructeur. """ number: Int! + + """ + Liste de tous les dossiers en attente de suppression définitive d’un groupe instructeur. + """ + pendingDeletedDossiers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Dossiers en attente de suppression depuis la date. + """ + deletedSince: ISO8601DateTime + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + L’ordre des dossiers en attente de suppression. + """ + order: Order = ASC + ): DeletedDossierConnection! } type HeaderSectionChampDescriptor implements ChampDescriptor { diff --git a/app/graphql/types/groupe_instructeur_with_dossiers_type.rb b/app/graphql/types/groupe_instructeur_with_dossiers_type.rb index 6889d8e7b..f80b38e02 100644 --- a/app/graphql/types/groupe_instructeur_with_dossiers_type.rb +++ b/app/graphql/types/groupe_instructeur_with_dossiers_type.rb @@ -18,6 +18,11 @@ module Types argument :deleted_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers supprimés depuis la date." end + field :pending_deleted_dossiers, Types::DeletedDossierType.connection_type, "Liste de tous les dossiers en attente de suppression définitive d’un groupe instructeur.", null: false do + argument :order, Types::Order, default_value: :asc, required: false, description: "L’ordre des dossiers en attente de suppression." + argument :deleted_since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers en attente de suppression depuis la date." + end + def dossiers(updated_since: nil, created_since: nil, state: nil, archived: nil, revision: nil, max_revision: nil, min_revision: nil, order:, lookahead:) dossiers = object .dossiers @@ -70,5 +75,15 @@ module Types dossiers.order(deleted_at: order) end + + def pending_deleted_dossiers(deleted_since: nil, order:) + dossiers = object.dossiers.hidden_for_administration + + if deleted_since.present? + dossiers = dossiers.hidden_since(deleted_since) + end + + dossiers.order(hidden_by_user_at: order, hidden_by_administration_at: order) + end end end diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb index 5dd0fcebb..3e96008c8 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -136,7 +136,7 @@ describe API::V2::GraphqlController do context 'include deleted Dossiers' do let(:variables) { { demarcheNumber: procedure.id, includeDeletedDossiers: true, deletedSince: 2.weeks.ago.iso8601 } } - let(:deleted_dossier) { create(:deleted_dossier, dossier: dossier_accepte) } + let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier_accepte, DeletedDossier.reasons.fetch(:user_request)) } before { deleted_dossier } @@ -191,6 +191,40 @@ describe API::V2::GraphqlController do expect(gql_data[:groupeInstructeur][:dossiers][:nodes].size).to eq(1) } end + + context 'include deleted Dossiers' do + let(:variables) { { groupeInstructeurNumber: groupe_instructeur.id, includeDeletedDossiers: true, deletedSince: 2.weeks.ago.iso8601 } } + let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier_accepte, DeletedDossier.reasons.fetch(:user_request)) } + + before { deleted_dossier } + + it { + expect(gql_errors).to be_nil + expect(gql_data[:groupeInstructeur][:id]).to eq(groupe_instructeur.to_typed_id) + expect(gql_data[:groupeInstructeur][:deletedDossiers][:nodes].size).to eq(1) + expect(gql_data[:groupeInstructeur][:deletedDossiers][:nodes].first[:id]).to eq(deleted_dossier.to_typed_id) + expect(gql_data[:groupeInstructeur][:deletedDossiers][:nodes].first[:dateSupression]).to eq(deleted_dossier.deleted_at.iso8601) + } + end + + context 'include pending deleted Dossiers' do + let(:variables) { { groupeInstructeurNumber: groupe_instructeur.id, includePendingDeletedDossiers: true, pendingDeletedSince: 2.weeks.ago.iso8601 } } + + before { + dossier.hide_and_keep_track!(dossier.user, DeletedDossier.reasons.fetch(:user_request)) + dossier_accepte.hide_and_keep_track!(instructeur, DeletedDossier.reasons.fetch(:instructeur_request)) + } + + it { + expect(gql_errors).to be_nil + expect(gql_data[:groupeInstructeur][:id]).to eq(groupe_instructeur.to_typed_id) + expect(gql_data[:groupeInstructeur][:pendingDeletedDossiers][:nodes].size).to eq(2) + expect(gql_data[:groupeInstructeur][:pendingDeletedDossiers][:nodes].first[:id]).to eq(GraphQL::Schema::UniqueWithinType.encode('DeletedDossier', dossier.id)) + expect(gql_data[:groupeInstructeur][:pendingDeletedDossiers][:nodes].second[:id]).to eq(GraphQL::Schema::UniqueWithinType.encode('DeletedDossier', dossier_accepte.id)) + expect(gql_data[:groupeInstructeur][:pendingDeletedDossiers][:nodes].first[:dateSupression]).to eq(dossier.hidden_by_user_at.iso8601) + expect(gql_data[:groupeInstructeur][:pendingDeletedDossiers][:nodes].second[:dateSupression]).to eq(dossier_accepte.hidden_by_administration_at.iso8601) + } + end end context 'getDemarcheDescriptor' do From ca9fa716674faa5e8ae07307f96facfeb591bd16 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 11 Apr 2023 10:11:43 +0200 Subject: [PATCH 07/75] feat(graphql): improuve stored query --- app/graphql/api/v2/stored_query.rb | 90 ++++++++++++++++++------------ 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/app/graphql/api/v2/stored_query.rb b/app/graphql/api/v2/stored_query.rb index 3197e6d05..f727b60be 100644 --- a/app/graphql/api/v2/stored_query.rb +++ b/app/graphql/api/v2/stored_query.rb @@ -23,18 +23,18 @@ class API::V2::StoredQuery $revision: ID $createdSince: ISO8601DateTime $updatedSince: ISO8601DateTime - $deletedOrder: Order - $deletedFirst: Int - $deletedAfter: String - $deletedSince: ISO8601DateTime $pendingDeletedOrder: Order $pendingDeletedFirst: Int $pendingDeletedAfter: String $pendingDeletedSince: ISO8601DateTime + $deletedOrder: Order + $deletedFirst: Int + $deletedAfter: String + $deletedSince: ISO8601DateTime $includeGroupeInstructeurs: Boolean = false $includeDossiers: Boolean = false - $includeDeletedDossiers: Boolean = false $includePendingDeletedDossiers: Boolean = false + $includeDeletedDossiers: Boolean = false $includeRevision: Boolean = false $includeService: Boolean = false $includeChamps: Boolean = true @@ -118,11 +118,16 @@ class API::V2::StoredQuery $revision: ID $createdSince: ISO8601DateTime $updatedSince: ISO8601DateTime + $pendingDeletedOrder: Order + $pendingDeletedFirst: Int + $pendingDeletedAfter: String + $pendingDeletedSince: ISO8601DateTime $deletedOrder: Order $deletedFirst: Int $deletedAfter: String $deletedSince: ISO8601DateTime $includeDossiers: Boolean = false + $includePendingDeletedDossiers: Boolean = false $includeDeletedDossiers: Boolean = false $includeChamps: Boolean = true $includeAnotations: Boolean = true @@ -157,6 +162,19 @@ class API::V2::StoredQuery ...DossierFragment } } + pendingDeletedDossiers( + order: $pendingDeletedOrder + first: $pendingDeletedFirst + after: $pendingDeletedAfter + deletedSince: $pendingDeletedSince + ) @include(if: $includePendingDeletedDossiers) { + pageInfo { + ...PageInfoFragment + } + nodes { + ...DeletedDossierFragment + } + } deletedDossiers( order: $deletedOrder first: $deletedFirst @@ -250,14 +268,9 @@ class API::V2::StoredQuery } demandeur { __typename - ... on PersonnePhysique { - civilite - nom - prenom - dateDeNaissance - } - ... on PersonneMoraleIncomplete { siret } + ...PersonnePhysiqueFragment ...PersonneMoraleFragment + ...PersonneMoraleIncompleteFragment } demarche { revision { @@ -368,24 +381,6 @@ class API::V2::StoredQuery collapsibleExplanationEnabled collapsibleExplanationText } - ... on PaysChampDescriptor { - options { - name - code - } - } - ... on RegionChampDescriptor { - options { - name - code - } - } - ... on DepartementChampDescriptor { - options { - name - code - } - } } fragment AvisFragment on Avis { @@ -502,8 +497,7 @@ class API::V2::StoredQuery } ... on EpciChamp { epci { - name - code + ...EpciFragment } departement { ...DepartementFragment @@ -524,14 +518,12 @@ class API::V2::StoredQuery } ... on RegionChamp { region { - name - code + ...RegionFragment } } ... on PaysChamp { pays { - name - code + ...PaysFragment } } ... on SiretChamp { @@ -579,6 +571,17 @@ class API::V2::StoredQuery } } + fragment PersonneMoraleIncompleteFragment on PersonneMoraleIncomplete { + siret + } + + fragment PersonnePhysiqueFragment on PersonnePhysique { + civilite + nom + prenom + } + + fragment FileFragment on File { filename contentType @@ -602,11 +605,26 @@ class API::V2::StoredQuery regionCode } + fragment PaysFragment on Pays { + name + code + } + + fragment RegionFragment on Region { + name + code + } + fragment DepartementFragment on Departement { name code } + fragment EpciFragment on Epci { + name + code + } + fragment CommuneFragment on Commune { name code From bd07441b7e3f838f1c611cd9d0a4d8aaf3f05dc5 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 18 Apr 2023 11:36:40 +0200 Subject: [PATCH 08/75] fix(graphql): stub metadata on fake files interfaces --- app/graphql/api/v2/stored_query.rb | 2 +- app/graphql/types/dossier_type.rb | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/graphql/api/v2/stored_query.rb b/app/graphql/api/v2/stored_query.rb index 3197e6d05..255e62e3c 100644 --- a/app/graphql/api/v2/stored_query.rb +++ b/app/graphql/api/v2/stored_query.rb @@ -240,7 +240,7 @@ class API::V2::StoredQuery ...FileFragment } pdf { - url + ...FileFragment } usager { email diff --git a/app/graphql/types/dossier_type.rb b/app/graphql/types/dossier_type.rb index e60117bac..0dc34f2c1 100644 --- a/app/graphql/types/dossier_type.rb +++ b/app/graphql/types/dossier_type.rb @@ -143,7 +143,10 @@ module Types { filename: "dossier-#{object.id}.pdf", content_type: 'application/pdf', - url: Rails.application.routes.url_helpers.api_v2_dossier_pdf_url(id: sgid) + url: Rails.application.routes.url_helpers.api_v2_dossier_pdf_url(id: sgid), + byte_size: 0, + byte_size_big_int: '0', + checksum: '' } end @@ -152,7 +155,10 @@ module Types { filename: "dossier-#{object.id}-features.json", content_type: 'application/json', - url: Rails.application.routes.url_helpers.api_v2_dossier_geojson_url(id: sgid) + url: Rails.application.routes.url_helpers.api_v2_dossier_geojson_url(id: sgid), + byte_size: 0, + byte_size_big_int: '0', + checksum: '' } end From 2a09f1d505aa2106f44a4984a91d33f594641b2c Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 18 Apr 2023 09:31:17 +0200 Subject: [PATCH 09/75] fix(graphql): return empty arrays from loaders when loading collections --- app/graphql/loaders/champ.rb | 2 +- app/graphql/loaders/record.rb | 2 +- spec/graphql/dossier_spec.rb | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/graphql/loaders/champ.rb b/app/graphql/loaders/champ.rb index b33672b10..5f5bee562 100644 --- a/app/graphql/loaders/champ.rb +++ b/app/graphql/loaders/champ.rb @@ -13,7 +13,7 @@ module Loaders def perform(keys) query(keys).each { |record| fulfill(record.stable_id, [record].compact) } - keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } + keys.each { |key| fulfill(key, []) unless fulfilled?(key) } end private diff --git a/app/graphql/loaders/record.rb b/app/graphql/loaders/record.rb index 821a9f1cd..4d6b94fa1 100644 --- a/app/graphql/loaders/record.rb +++ b/app/graphql/loaders/record.rb @@ -21,7 +21,7 @@ module Loaders fulfilled_value = @array ? [record].compact : record fulfill(record.public_send(@column), fulfilled_value) end - keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } + keys.each { |key| fulfill(key, @array ? [] : nil) unless fulfilled?(key) } end private diff --git a/spec/graphql/dossier_spec.rb b/spec/graphql/dossier_spec.rb index 105bcf91c..6e8e0f723 100644 --- a/spec/graphql/dossier_spec.rb +++ b/spec/graphql/dossier_spec.rb @@ -75,6 +75,27 @@ RSpec.describe Types::DossierType, type: :graphql do end end + describe 'dossier with selected champ' do + let(:procedure) { create(:procedure, types_de_champ_public: [{ libelle: 'yolo' }, { libelle: 'toto' }]) } + let(:dossier) { create(:dossier, :en_construction, :with_populated_champs, procedure:) } + let(:query) { DOSSIER_WITH_SELECTED_CHAMP_QUERY } + let(:variables) { { number: dossier.id, id: champ.to_typed_id } } + let(:champ) { dossier.champs_public.last } + + context 'when champ exists' do + it { + expect(data[:dossier][:champs].size).to eq 1 + expect(data[:dossier][:champs][0][:label]).to eq "toto" + } + end + + context "when champ dosen't exists" do + let(:variables) { { number: dossier.id, id: '1234' } } + + it { expect(data[:dossier][:champs].size).to eq 0 } + end + end + describe 'dossier with conditional champs' do include Logic let(:stable_id) { 1234 } @@ -390,4 +411,15 @@ RSpec.describe Types::DossierType, type: :graphql do } } GRAPHQL + + DOSSIER_WITH_SELECTED_CHAMP_QUERY = <<-GRAPHQL + query($number: Int!, $id: ID!) { + dossier(number: $number) { + champs(id: $id) { + id + label + } + } + } + GRAPHQL end From fbae6d941d76f3f315d3f13c7212ac7eb2f73fdc Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 18 Apr 2023 09:48:38 +0200 Subject: [PATCH 10/75] feat(graphql): add error codes to graphql errors --- app/controllers/api/v2/graphql_controller.rb | 36 ++++++++++++++----- app/graphql/api/v2/schema.rb | 2 +- app/graphql/api/v2/stored_query.rb | 2 +- app/graphql/types/base_object.rb | 6 ++++ .../api/v2/graphql_controller_spec.rb | 2 +- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v2/graphql_controller.rb b/app/controllers/api/v2/graphql_controller.rb index ce1b7001b..6d858ab10 100644 --- a/app/controllers/api/v2/graphql_controller.rb +++ b/app/controllers/api/v2/graphql_controller.rb @@ -5,7 +5,9 @@ class API::V2::GraphqlController < API::V2::BaseController render json: result rescue GraphQL::ParseError, JSON::ParserError => exception - handle_parse_error(exception) + handle_parse_error(exception, :graphql_parse_failed) + rescue ArgumentError => exception + handle_parse_error(exception, :bad_request) rescue => exception if Rails.env.production? handle_error_in_production(exception) @@ -33,7 +35,12 @@ class API::V2::GraphqlController < API::V2::BaseController rescue ActionDispatch::Http::Parameters::ParseError => exception render json: { errors: [ - { message: exception.cause.message } + { + message: exception.cause.message, + extensions: { + code: :bad_request + } + } ], data: nil }, status: 400 @@ -75,10 +82,13 @@ class API::V2::GraphqlController < API::V2::BaseController end end - def handle_parse_error(exception) + def handle_parse_error(exception, code) render json: { errors: [ - { message: exception.message } + { + message: exception.message, + extensions: { code: } + } ], data: nil }, status: 400 @@ -90,22 +100,32 @@ class API::V2::GraphqlController < API::V2::BaseController render json: { errors: [ - { message: exception.message, backtrace: exception.backtrace } + { + message: exception.message, + extensions: { + code: :internal_server_error, + backtrace: exception.backtrace + } + } ], data: nil }, status: 500 end def handle_error_in_production(exception) - extra = { exception_id: SecureRandom.uuid } - Sentry.capture_exception(exception, extra:) + exception_id = SecureRandom.uuid + Sentry.with_scope do |scope| + scope.set_tags(exception_id:) + Sentry.capture_exception(exception) + end render json: { errors: [ { message: "Internal Server Error", extensions: { - exception: { id: extra[:exception_id] } + code: :internal_server_error, + exception_id: } } ], diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb index af9d21d54..1f418e377 100644 --- a/app/graphql/api/v2/schema.rb +++ b/app/graphql/api/v2/schema.rb @@ -50,7 +50,7 @@ class API::V2::Schema < GraphQL::Schema when GroupeInstructeur Types::GroupeInstructeurType else - raise GraphQL::ExecutionError.new("Unexpected object: #{object}") + type_definition end end diff --git a/app/graphql/api/v2/stored_query.rb b/app/graphql/api/v2/stored_query.rb index 255e62e3c..1f5effe5c 100644 --- a/app/graphql/api/v2/stored_query.rb +++ b/app/graphql/api/v2/stored_query.rb @@ -8,7 +8,7 @@ class API::V2::StoredQuery when 'introspection' GraphQL::Introspection::INTROSPECTION_QUERY else - raise GraphQL::ExecutionError.new("No query with id \"#{query_id}\"") + raise GraphQL::ExecutionError.new("No query with id \"#{query_id}\"", extensions: { code: :bad_request }) end end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 2d20f1c05..88a6be02a 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -1,5 +1,11 @@ module Types class BaseObject < GraphQL::Schema::Object field_class BaseField + + class InvalidNullError < GraphQL::InvalidNullError + def to_h + super.merge(extensions: { code: :invalid_null }) + end + end end end diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index 869b2d39f..f91064be9 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -684,7 +684,7 @@ describe API::V2::GraphqlController do end it "should return an error" do - expect(gql_errors).to eq([{ message: "Cannot return null for non-nullable field PersonneMorale.siegeSocial" }]) + expect(gql_errors).to eq([{ message: "Cannot return null for non-nullable field PersonneMorale.siegeSocial", extensions: { code: "invalid_null" } }]) end end end From f70532a8442696ddb27b982e831e0f8b99fc10c0 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 18 Apr 2023 10:08:18 +0200 Subject: [PATCH 11/75] feat(graphql): global not found error handling --- app/graphql/api/v2/schema.rb | 6 ++-- app/graphql/types/query_type.rb | 8 +---- .../graphql_controller_stored_queries_spec.rb | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb index 1f418e377..25779ff04 100644 --- a/app/graphql/api/v2/schema.rb +++ b/app/graphql/api/v2/schema.rb @@ -25,8 +25,6 @@ class API::V2::Schema < GraphQL::Schema def self.object_from_id(id, ctx) ApplicationRecord.record_from_typed_id(id) - rescue => e - raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found }) end def self.resolve_type(type_definition, object, ctx) @@ -129,6 +127,10 @@ class API::V2::Schema < GraphQL::Schema super end + rescue_from(ActiveRecord::RecordNotFound) do |_error, _object, _args, _ctx, field| + raise GraphQL::ExecutionError.new("#{field.type.unwrap.graphql_name} not found", extensions: { code: :not_found }) + end + class Timeout < GraphQL::Schema::Timeout def handle_timeout(error, query) Sentry.capture_exception(error, extra: query.context.query_info) diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 47b7c1b9f..044eae3a6 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -26,13 +26,11 @@ module Types demarche_number = demarche.number.presence || ApplicationRecord.id_from_typed_id(demarche.id) Procedure .includes(draft_revision: :procedure, published_revision: :procedure) - .find_by(id: demarche_number) + .find(demarche_number) end def demarche(number:) Procedure.for_api_v2.find(number) - rescue => e - raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found }) end def dossier(number:) @@ -42,14 +40,10 @@ module Types Dossier.visible_by_administration.for_api_v2.find(number) end DossierPreloader.load_one(dossier) - rescue => e - raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found }) end def groupe_instructeur(number:) GroupeInstructeur.for_api_v2.find(number) - rescue => e - raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found }) end def self.accessible?(context) diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb index 5dd0fcebb..7b790db02 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -76,6 +76,15 @@ describe API::V2::GraphqlController do expect(gql_data[:dossier][:demandeur][:prenom]).to eq(dossier.individual.prenom) } + context 'not found' do + let(:variables) { { dossierNumber: 0 } } + + it { + expect(gql_errors.first[:message]).to eq('Dossier not found') + expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' }) + } + end + context 'with entreprise' do let(:procedure) { create(:procedure, :published, :with_service, administrateurs: [admin], types_de_champ_public:) } let(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) } @@ -114,6 +123,15 @@ describe API::V2::GraphqlController do expect(gql_data[:demarche][:dossiers]).to be_nil } + context 'not found' do + let(:variables) { { demarcheNumber: 0 } } + + it { + expect(gql_errors.first[:message]).to eq('Demarche not found') + expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' }) + } + end + context 'include Dossiers' do let(:variables) { { demarcheNumber: procedure.id, includeDossiers: true } } @@ -182,6 +200,15 @@ describe API::V2::GraphqlController do expect(gql_data[:groupeInstructeur][:dossiers]).to be_nil } + context 'not found' do + let(:variables) { { groupeInstructeurNumber: 0 } } + + it { + expect(gql_errors.first[:message]).to eq('GroupeInstructeurWithDossiers not found') + expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' }) + } + end + context 'include Dossiers' do let(:variables) { { groupeInstructeurNumber: groupe_instructeur.id, includeDossiers: true } } @@ -207,6 +234,15 @@ describe API::V2::GraphqlController do } end + context 'not found' do + let(:variables) { { demarche: { number: 0 } } } + + it { + expect(gql_errors.first[:message]).to eq('DemarcheDescriptor not found') + expect(gql_errors.first[:extensions]).to eq({ code: 'not_found' }) + } + end + context 'find by id' do let(:variables) { { demarche: { id: procedure.to_typed_id } } } From ae1ec87397dfc32541d986554dc3bffa52ae9a46 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 18 Apr 2023 10:27:20 +0200 Subject: [PATCH 12/75] feat(graphql): add code to timeout errors --- app/graphql/api/v2/schema.rb | 1 + .../api/v2/graphql_controller_stored_queries_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb index 25779ff04..9e81aaa49 100644 --- a/app/graphql/api/v2/schema.rb +++ b/app/graphql/api/v2/schema.rb @@ -133,6 +133,7 @@ class API::V2::Schema < GraphQL::Schema class Timeout < GraphQL::Schema::Timeout def handle_timeout(error, query) + error.extensions = { code: :timeout } Sentry.capture_exception(error, extra: query.context.query_info) end end diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb index 7b790db02..f28c8c393 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -64,6 +64,18 @@ describe API::V2::GraphqlController do } end + context 'timeout' do + let(:variables) { { dossierNumber: dossier.id } } + let(:operation_name) { 'getDossier' } + + before { allow_any_instance_of(API::V2::Schema::Timeout).to receive(:max_seconds).and_return(0) } + + it { + expect(gql_errors.first[:message]).to eq('Timeout on Query.dossier') + expect(gql_errors.first[:extensions]).to eq({ code: 'timeout' }) + } + end + context 'getDossier' do let(:variables) { { dossierNumber: dossier.id } } let(:operation_name) { 'getDossier' } From eb599394e08405a01c879bab951b8f56cbca9f96 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 29 Mar 2023 23:52:19 +0200 Subject: [PATCH 13/75] feat(dossier): use turbo on linked lists --- .../linked_drop_down_list_component.rb | 13 ++++ .../linked_drop_down_list_component.html.haml | 26 ++++---- .../controllers/champ_dropdown_controller.ts | 62 +------------------ app/models/type_de_champ.rb | 3 +- 4 files changed, 27 insertions(+), 77 deletions(-) diff --git a/app/components/editable_champ/linked_drop_down_list_component.rb b/app/components/editable_champ/linked_drop_down_list_component.rb index a8d5a89a0..d59781329 100644 --- a/app/components/editable_champ/linked_drop_down_list_component.rb +++ b/app/components/editable_champ/linked_drop_down_list_component.rb @@ -1,2 +1,15 @@ class EditableChamp::LinkedDropDownListComponent < EditableChamp::EditableChampBaseComponent + private + + def secondary_label + secondary_label_text + secondary_label_mandatory + end + + def secondary_label_text + @champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première" + end + + def secondary_label_mandatory + @champ.mandatory? ? tag.span(' *', class: 'mandatory') : '' + end end diff --git a/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml b/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml index 21c23d0d6..4ed612278 100644 --- a/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml +++ b/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml @@ -1,16 +1,12 @@ - if @champ.options? - = @form.select :primary_value, - @champ.primary_options, - {}, - { data: { secondary_options: @champ.secondary_options }, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id } } - - .secondary{ class: @champ.has_secondary_options_for_primary? ? '' : 'hidden' } - = @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do - - sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.mandatory? ? tag.span(' *', class: 'mandatory') : '')) - - if @champ.drop_down_secondary_description.present? - .notice{ id: "#{@champ.describedby_id}-secondary" }= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true) - = @form.select :secondary_value, - @champ.secondary_options[@champ.primary_value], - {}, - { data: { secondary: true }, disabled: !@champ.has_secondary_options_for_primary?, required: @champ.required?, id: "#{@champ.input_id}-secondary", aria: { describedby: "#{@champ.describedby_id}-secondary" } } - = @form.hidden_field :secondary_value, value: '', disabled: @champ.has_secondary_options_for_primary? + = @form.select :primary_value, @champ.primary_options, {}, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id } + - if @champ.has_secondary_options_for_primary? + .secondary + = @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do + - sanitize(secondary_label) + - if @champ.drop_down_secondary_description.present? + .notice{ id: "#{@champ.describedby_id}-secondary" } + = render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true) + = @form.select :secondary_value, @champ.secondary_options[@champ.primary_value], {}, required: @champ.required?, id: "#{@champ.input_id}-secondary", aria: { describedby: "#{@champ.describedby_id}-secondary" } + - else + = @form.hidden_field :secondary_value, value: '' diff --git a/app/javascript/controllers/champ_dropdown_controller.ts b/app/javascript/controllers/champ_dropdown_controller.ts index b9dbfe1e4..c65b7dd95 100644 --- a/app/javascript/controllers/champ_dropdown_controller.ts +++ b/app/javascript/controllers/champ_dropdown_controller.ts @@ -2,11 +2,8 @@ import { isSelectElement, isCheckboxOrRadioInputElement, show, - hide, - enable, - disable + hide } from '@utils'; -import { z } from 'zod'; import { ApplicationController } from './application_controller'; @@ -20,7 +17,6 @@ export class ChampDropdownController extends ApplicationController { if (!target.disabled) { if (isSelectElement(target) || isCheckboxOrRadioInputElement(target)) { this.toggleOtherInput(target); - this.toggleLinkedSelect(target); } } } @@ -42,60 +38,4 @@ export class ChampDropdownController extends ApplicationController { } } } - - private toggleLinkedSelect(target: HTMLSelectElement | HTMLInputElement) { - const secondaryOptions = target.dataset.secondaryOptions; - if (isSelectElement(target) && secondaryOptions) { - const parent = target.closest('.editable-champ-linked_drop_down_list'); - const secondary = parent?.querySelector( - 'select[data-secondary]' - ); - if (secondary) { - const options = parseOptions(secondaryOptions); - this.setSecondaryOptions(secondary, options[target.value]); - } - } - } - - private setSecondaryOptions( - secondarySelectElement: HTMLSelectElement, - options: string[] - ) { - const wrapper = secondarySelectElement.closest('.secondary'); - const hidden = wrapper?.nextElementSibling as HTMLInputElement | null; - - secondarySelectElement.innerHTML = ''; - - if (options.length) { - disable(hidden); - - if (secondarySelectElement.required) { - secondarySelectElement.appendChild(makeOption('')); - } - for (const option of options) { - secondarySelectElement.appendChild(makeOption(option)); - } - - secondarySelectElement.selectedIndex = 0; - enable(secondarySelectElement); - show(wrapper); - } else { - hide(wrapper); - disable(secondarySelectElement); - enable(hidden); - } - } -} - -const SecondaryOptions = z.record(z.string().array()); - -function parseOptions(options: string) { - return SecondaryOptions.parse(JSON.parse(options)); -} - -function makeOption(option: string) { - const element = document.createElement('option'); - element.textContent = option; - element.value = option; - return element; } diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index badf1f6e3..b3ec31428 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -503,7 +503,8 @@ class TypeDeChamp < ApplicationRecord when type_champs.fetch(:epci), type_champs.fetch(:communes), type_champs.fetch(:multiple_drop_down_list), - type_champs.fetch(:dossier_link) + type_champs.fetch(:dossier_link), + type_champs.fetch(:linked_drop_down_list) true else false From 0912a30eb0ede46d98c03b6b80bf3a9858db0c26 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 30 Mar 2023 00:24:11 +0200 Subject: [PATCH 14/75] feat(dossier): use turbo on select with other --- .../drop_down_list_component.html.haml | 4 +- .../drop_down_other_input_component.rb | 3 ++ .../drop_down_other_input_component.html.haml | 4 +- .../editable_champ_component.rb | 5 --- .../controllers/champ_dropdown_controller.ts | 41 ------------------- app/models/champs/drop_down_list_champ.rb | 19 ++++++--- app/models/type_de_champ.rb | 3 +- .../editable_champ_component_spec.rb | 8 ++-- spec/system/users/dropdown_spec.rb | 2 +- 9 files changed, 27 insertions(+), 62 deletions(-) delete mode 100644 app/javascript/controllers/champ_dropdown_controller.ts diff --git a/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml b/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml index 286a437e0..f2369b33c 100644 --- a/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml +++ b/app/components/editable_champ/drop_down_list_component/drop_down_list_component.html.haml @@ -8,12 +8,12 @@ - if !@champ.mandatory? %label.blank-radio - = @form.radio_button :value, '' + = @form.radio_button :value, '', checked: @champ.value.blank? && !@champ.other? Non renseigné - if @champ.drop_down_other? %label - = @form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: @champ.other_value_present? + = @form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: @champ.other? Autre - else = @form.select :value, @champ.options_without_empty_value_when_mandatory(@champ.options), { selected: @champ.selected }, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id } diff --git a/app/components/editable_champ/drop_down_other_input_component.rb b/app/components/editable_champ/drop_down_other_input_component.rb index 94e695d4c..bb8f5665e 100644 --- a/app/components/editable_champ/drop_down_other_input_component.rb +++ b/app/components/editable_champ/drop_down_other_input_component.rb @@ -1,2 +1,5 @@ class EditableChamp::DropDownOtherInputComponent < EditableChamp::EditableChampBaseComponent + def render? + @champ.other? + end end diff --git a/app/components/editable_champ/drop_down_other_input_component/drop_down_other_input_component.html.haml b/app/components/editable_champ/drop_down_other_input_component/drop_down_other_input_component.html.haml index 168c008dc..f0160046b 100644 --- a/app/components/editable_champ/drop_down_other_input_component/drop_down_other_input_component.html.haml +++ b/app/components/editable_champ/drop_down_other_input_component/drop_down_other_input_component.html.haml @@ -1,4 +1,4 @@ -.drop_down_other{ class: @champ.other_value_present? ? '' : 'hidden' } +.drop_down_other .notice %label{ for: dom_id(@champ, :value_other) } Veuillez saisir votre autre choix - = @form.text_field :value_other, maxlength: 200, size: nil, id: dom_id(@champ, :value_other), disabled: !@champ.other_value_present? + = @form.text_field :value_other, maxlength: 200, size: nil, id: dom_id(@champ, :value_other) diff --git a/app/components/editable_champ/editable_champ_component.rb b/app/components/editable_champ/editable_champ_component.rb index 622447493..1b1e78132 100644 --- a/app/components/editable_champ/editable_champ_component.rb +++ b/app/components/editable_champ/editable_champ_component.rb @@ -30,11 +30,6 @@ class EditableChamp::EditableChampComponent < ApplicationComponent # This is an editable champ. Lets find what controllers it might need. controllers = ['autosave'] - # This is a dropdown champ. Activate special behaviours it might have. - if @champ.simple_drop_down_list? || @champ.linked_drop_down_list? - controllers << 'champ-dropdown' - end - controllers.join(' ') end end diff --git a/app/javascript/controllers/champ_dropdown_controller.ts b/app/javascript/controllers/champ_dropdown_controller.ts deleted file mode 100644 index c65b7dd95..000000000 --- a/app/javascript/controllers/champ_dropdown_controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - isSelectElement, - isCheckboxOrRadioInputElement, - show, - hide -} from '@utils'; - -import { ApplicationController } from './application_controller'; - -export class ChampDropdownController extends ApplicationController { - connect() { - this.on('change', (event) => this.onChange(event)); - } - - private onChange(event: Event) { - const target = event.target as HTMLInputElement; - if (!target.disabled) { - if (isSelectElement(target) || isCheckboxOrRadioInputElement(target)) { - this.toggleOtherInput(target); - } - } - } - - private toggleOtherInput(target: HTMLSelectElement | HTMLInputElement) { - const parent = target.closest('.editable-champ-drop_down_list'); - const inputGroup = parent?.querySelector('.drop_down_other'); - if (inputGroup) { - const input = inputGroup.querySelector('input'); - if (input) { - if (target.value == '__other__') { - show(inputGroup); - input.disabled = false; - input.focus(); - } else { - hide(inputGroup); - input.disabled = true; - } - } - } - } -} diff --git a/app/models/champs/drop_down_list_champ.rb b/app/models/champs/drop_down_list_champ.rb index 23d4a27bf..467643673 100644 --- a/app/models/champs/drop_down_list_champ.rb +++ b/app/models/champs/drop_down_list_champ.rb @@ -21,6 +21,7 @@ # type_de_champ_id :integer # class Champs::DropDownListChamp < Champ + store_accessor :value_json, :other THRESHOLD_NB_OPTIONS_AS_RADIO = 5 OTHER = '__other__' delegate :options_without_empty_value_when_mandatory, to: :type_de_champ @@ -44,7 +45,7 @@ class Champs::DropDownListChamp < Champ end def selected - other_value_present? ? OTHER : value + other? ? OTHER : value end def disabled_options @@ -55,22 +56,28 @@ class Champs::DropDownListChamp < Champ drop_down_list_enabled_non_empty_options end - def other_value_present? - drop_down_other? && value.present? && drop_down_list_options.exclude?(value) + def other? + drop_down_other? && (other || (value.present? && drop_down_list_options.exclude?(value))) end def value=(value) - if value != OTHER + if value == OTHER + self.other = true + write_attribute(:value, nil) + else + self.other = false write_attribute(:value, value) end end def value_other=(value) - write_attribute(:value, value) + if other? + write_attribute(:value, value) + end end def value_other - other_value_present? ? value : "" + other? ? value : "" end def in?(options) diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index b3ec31428..3b00828c5 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -504,7 +504,8 @@ class TypeDeChamp < ApplicationRecord type_champs.fetch(:communes), type_champs.fetch(:multiple_drop_down_list), type_champs.fetch(:dossier_link), - type_champs.fetch(:linked_drop_down_list) + type_champs.fetch(:linked_drop_down_list), + type_champs.fetch(:drop_down_list) true else false diff --git a/spec/components/editable_champ/editable_champ_component_spec.rb b/spec/components/editable_champ/editable_champ_component_spec.rb index 77a6d0645..de9250670 100644 --- a/spec/components/editable_champ/editable_champ_component_spec.rb +++ b/spec/components/editable_champ/editable_champ_component_spec.rb @@ -34,14 +34,14 @@ describe EditableChamp::EditableChampComponent, type: :component do it { expect(subject).to eq(data) } context 'when a public dropdown champ' do - let(:controllers) { ['autosave', 'champ-dropdown'] } + let(:controllers) { ['autosave'] } let(:champ) { create(:champ_drop_down_list, dossier: dossier) } it { expect(subject).to eq(data) } end context 'when a private dropdown champ' do - let(:controllers) { ['autosave', 'champ-dropdown'] } + let(:controllers) { ['autosave'] } let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) } it { expect(subject).to eq(data) } @@ -49,14 +49,14 @@ describe EditableChamp::EditableChampComponent, type: :component do end context 'when a public dropdown champ' do - let(:controllers) { ['autosave', 'champ-dropdown'] } + let(:controllers) { ['autosave'] } let(:champ) { create(:champ_drop_down_list, dossier: dossier) } it { expect(subject).to eq(data) } end context 'when a private dropdown champ' do - let(:controllers) { ['autosave', 'champ-dropdown'] } + let(:controllers) { ['autosave'] } let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) } it { expect(subject).to eq(data) } diff --git a/spec/system/users/dropdown_spec.rb b/spec/system/users/dropdown_spec.rb index dc5cb0735..e0134bcb2 100644 --- a/spec/system/users/dropdown_spec.rb +++ b/spec/system/users/dropdown_spec.rb @@ -59,7 +59,7 @@ describe 'dropdown list with other option activated', js: true do scenario 'with a select and other, selecting a value save it (avoid hidden other_value to be sent)' do fill_individual - find(".drop_down_other input", visible: false) + expect(page).not_to have_selector(".drop_down_other input") select("Autre") find(".drop_down_other input", visible: true) From c6aff63ce72bf208b50897f49526fa5dcd7adcf3 Mon Sep 17 00:00:00 2001 From: Julie Salha Date: Tue, 18 Apr 2023 17:25:20 +0200 Subject: [PATCH 15/75] remove phone contact accessibility statement --- app/views/static_pages/accessibility_statement.html.haml | 3 --- config/locales/en.yml | 1 - config/locales/fr.yml | 1 - 3 files changed, 5 deletions(-) diff --git a/app/views/static_pages/accessibility_statement.html.haml b/app/views/static_pages/accessibility_statement.html.haml index 9e18dac10..2db302701 100644 --- a/app/views/static_pages/accessibility_statement.html.haml +++ b/app/views/static_pages/accessibility_statement.html.haml @@ -124,9 +124,6 @@ %li = t('views.accessibility_statement.contact.infos.email_html') = mail_to CONTACT_EMAIL - %li - = t('views.accessibility_statement.contact.infos.phone_html') - = phone_to CONTACT_PHONE.gsub(" ", ""), CONTACT_PHONE %li = t('views.accessibility_statement.contact.infos.adress_html') diff --git a/config/locales/en.yml b/config/locales/en.yml index f5d189fb9..0f57fb79c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -191,7 +191,6 @@ en: intro: "If you are unable to access a piece of content or service, you can contact the Simplified Approaches manager to be directed to an accessible alternative or obtain the content in another form." infos: email_html: "By email:" - phone_html: "Contact DINUM by phone:" adress_html: "By post : DINUM , 20 avenue de Ségur 75007 Paris" remedies: title: "Remedies" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index cd52c4f3a..84c7b1dc1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -187,7 +187,6 @@ fr: intro: "Si vous n’arrivez pas à accéder à un contenu ou à un service, vous pouvez contacter le responsable de Démarches simplifiées pour être orienté vers une alternative accessible ou obtenir le contenu sous une autre forme." infos: email_html: "Par voie électronique :" - phone_html: "Contacter la DINUM par téléphone :" adress_html: "Par voie postale : DINUM , 20 avenue de Ségur 75007 Paris" remedies: title: "Voies de recours" From 8fa2bbb67d0fe97ab488eee29ed4943c58d06572 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 3 Apr 2023 16:25:45 +0200 Subject: [PATCH 16/75] amelioration(dolist): ne log erreurs pas les erreurs dans sentry lorsque le contact chez dolist est injoingable ou hardbounce --- app/lib/dolist/api.rb | 38 +++++++++++++++++++++++++ config/initializers/dolist.rb | 15 ++++++++-- spec/mailers/application_mailer_spec.rb | 31 ++++++++++++++------ 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/app/lib/dolist/api.rb b/app/lib/dolist/api.rb index 231a328f5..48d07d5fc 100644 --- a/app/lib/dolist/api.rb +++ b/app/lib/dolist/api.rb @@ -4,6 +4,7 @@ class Dolist::API CONTACT_URL = "https://apiv9.dolist.net/v1/contacts/read?AccountID=%{account_id}" EMAIL_LOGS_URL = "https://apiv9.dolist.net/v1/statistics/email/sendings/transactional/search?AccountID=%{account_id}" EMAIL_KEY = 7 + STATUS_KEY = 72 DOLIST_WEB_DASHBOARD = "https://campaign.dolist.net/#/%{account_id}/contacts/%{contact_id}/sendings" EMAIL_MESSAGES_ADRESSES_REPLIES = "https://apiv9.dolist.net/v1/email/messages/addresses/replies?AccountID=%{account_id}" EMAIL_MESSAGES_ADRESSES_PACKSENDERS = "https://apiv9.dolist.net/v1/email/messages/addresses/packsenders?AccountID=%{account_id}" @@ -13,6 +14,18 @@ class Dolist::API class_attribute :limit_remaining, :limit_reset_at + # those code are just undocumented + IGNORABLE_API_ERROR_CODE = [ + "458", + "402" + ] + + # see: https://usercampaign.dolist.net/wp-content/uploads/2022/12/Comprendre-les-Opt-out-tableau-v2.pdf + IGNORABLE_CONTACT_STATUSES = [ + "4", # Le serveur distant n'accepte pas le mail car il identifie que l’adresse e-mail est en erreur. + "7" # Suite à un envoi, le serveur distant accepte le mail dans un premier temps mais envoie une erreur définitive car l’adresse e-mail est en erreur. L'adresse e-mail n’existe pas ou n'existe plus. + ] + class << self def save_rate_limit_headers(headers) self.limit_remaining = headers["X-Rate-Limit-Remaining"].to_i @@ -124,6 +137,31 @@ class Dolist::API get format_url(EMAIL_MESSAGES_ADRESSES_REPLIES) end + # Une adresse e-mail peut ne pas être adressable pour différentes raisons (injoignable, plainte pour spam, blocage d’un FAI). + # Dans ce cas l’API d’envoi transactionnel renvoie différentes erreurs. + # Pour connaitre exactement le statut d’une adresse, je vous invite à récupérer le champ 72 du contact à partir de son adresse e-mail avec la méthode https://api.dolist.com/documentation/index.html#/40e7751d00dc3-rechercher-un-contact + # + # La liste des différents statuts est disponible sur https://usercampaign.dolist.net/wp-content/uploads/2022/12/Comprendre-les-Opt-out-tableau-v2.pdf + def fetch_contact_status(email_address) + url = format(Dolist::API::CONTACT_URL, account_id: account_id) + body = { + Query: { + FieldValueList: [{ ID: 7, Value: email_address }], + OutputFieldIDList: [72] + } + }.to_json + + post(url, body)["FieldList"].find { _1['ID'] == 72 }['Value'] + end + + def ignorable_api_error_code?(api_error_code) + IGNORABLE_API_ERROR_CODE.include?(api_error_code) + end + + def ignorable_contact_status?(contact_status) + IGNORABLE_CONTACT_STATUSES.include?(contact_status) + end + private def format_url(base) diff --git a/config/initializers/dolist.rb b/config/initializers/dolist.rb index 24471b651..3e04792aa 100644 --- a/config/initializers/dolist.rb +++ b/config/initializers/dolist.rb @@ -15,12 +15,21 @@ ActiveSupport.on_load(:action_mailer) do def initialize(mail); end def deliver!(mail) - response = Dolist::API.new.send_email(mail) - + client = Dolist::API.new + response = client.send_email(mail) if response&.dig("Result") mail.message_id = response.dig("Result") else - fail "DoList delivery error. Body: #{response}" + error_code = response&.dig("ResponseStatus", "ErrorCode") + + contact_status = if client.ignorable_api_error_code?(error_code) + client.fetch_contact_status(mail.to.first) + else + nil + end + if !client.ignorable_contact_status?(contact_status) + fail "DoList delivery error. Body: #{response}" + end end end end diff --git a/spec/mailers/application_mailer_spec.rb b/spec/mailers/application_mailer_spec.rb index 347d845af..53b33e2b5 100644 --- a/spec/mailers/application_mailer_spec.rb +++ b/spec/mailers/application_mailer_spec.rb @@ -28,16 +28,31 @@ RSpec.describe ApplicationMailer, type: :mailer do describe 'dealing with Dolist API error' do let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) } - before do - ActionMailer::Base.delivery_method = :dolist_api - api_error_response = { "ResponseStatus": { "ErrorCode": "Forbidden", "Message": "Blocked non authorized request", "Errors": [] } } - allow_any_instance_of(Dolist::API).to receive(:send_email).and_return(api_error_response) - end subject { DossierMailer.with(dossier:).notify_new_draft.deliver_now } + context 'not ignored error' do + before do + ActionMailer::Base.delivery_method = :dolist_api + api_error_response = { "ResponseStatus": { "ErrorCode": "Forbidden", "Message": "Blocked non authorized request", "Errors": [] } } + allow_any_instance_of(Dolist::API).to receive(:send_email).and_return(api_error_response) + end - it 'raise classic error to retry' do - expect { subject }.to raise_error(MailDeliveryError) - expect(EmailEvent.dolist_api.dispatch_error.count).to eq(1) + it 'raise classic error to retry' do + expect { subject }.to raise_error(MailDeliveryError) + expect(EmailEvent.dolist_api.dispatch_error.count).to eq(1) + end + end + + context 'ignored error' do + before do + ActionMailer::Base.delivery_method = :dolist_api + api_error_response = { "ResponseStatus" => { "ErrorCode" => "458", "Message" => "The contact is disabled.", "Errors" => [] } } + allow_any_instance_of(Dolist::API).to receive(:send_email).and_return(api_error_response) + allow_any_instance_of(Dolist::API).to receive(:fetch_contact_status).with(dossier.user.email).and_return("7") + end + + it 'does not raise' do + expect { subject }.not_to raise_error + end end end From 534ce34f87eddb1d3c0462c4546d30ca683dd701 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 18 Apr 2023 16:15:47 +0200 Subject: [PATCH 17/75] =?UTF-8?q?amelioration(Dolist::ApiSender):=20l?= =?UTF-8?q?=C3=A8ve=20une=20Dolist::IgnorableError=20afin=20de=20l'inscrir?= =?UTF-8?q?e=20dans=20l'historique=20des=20EmailEvent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/dolist/api.rb | 14 ++++++++++++-- app/mailers/concerns/mailer_monitoring_concern.rb | 8 +++++++- config/initializers/dolist.rb | 12 ++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/lib/dolist/api.rb b/app/lib/dolist/api.rb index 48d07d5fc..388271f67 100644 --- a/app/lib/dolist/api.rb +++ b/app/lib/dolist/api.rb @@ -154,6 +154,18 @@ class Dolist::API post(url, body)["FieldList"].find { _1['ID'] == 72 }['Value'] end + def ignorable_error?(response, mail) + error_code = response&.dig("ResponseStatus", "ErrorCode") + invalid_contact_status = if ignorable_api_error_code?(error_code) + fetch_contact_status(mail.to.first) + else + nil + end + [error_code, invalid_contact_status] + end + + private + def ignorable_api_error_code?(api_error_code) IGNORABLE_API_ERROR_CODE.include?(api_error_code) end @@ -162,8 +174,6 @@ class Dolist::API IGNORABLE_CONTACT_STATUSES.include?(contact_status) end - private - def format_url(base) format(base, account_id: account_id) end diff --git a/app/mailers/concerns/mailer_monitoring_concern.rb b/app/mailers/concerns/mailer_monitoring_concern.rb index 6cbd60247..f140c20fa 100644 --- a/app/mailers/concerns/mailer_monitoring_concern.rb +++ b/app/mailers/concerns/mailer_monitoring_concern.rb @@ -19,12 +19,18 @@ module MailerMonitoringConcern end end + rescue_from Dolist::IgnorableError, with: :log_delivery_error + def log_and_raise_delivery_error(exception) - EmailEvent.create_from_message!(message, status: "dispatch_error") + log_delivery_error(exception) Sentry.capture_exception(exception, extra: { to: message.to, subject: message.subject }) # re-raise another error so job will retry later raise MailDeliveryError.new(exception) end + + def log_delivery_error(exception) + EmailEvent.create_from_message!(message, status: "dispatch_error") + end end end diff --git a/config/initializers/dolist.rb b/config/initializers/dolist.rb index 3e04792aa..cdf1f2384 100644 --- a/config/initializers/dolist.rb +++ b/config/initializers/dolist.rb @@ -1,5 +1,8 @@ ActiveSupport.on_load(:action_mailer) do module Dolist + class IgnorableError < StandardError + end + class SMTP < ::Mail::SMTP def deliver!(mail) mail.from(ENV['DOLIST_NO_REPLY_EMAIL']) @@ -20,14 +23,11 @@ ActiveSupport.on_load(:action_mailer) do if response&.dig("Result") mail.message_id = response.dig("Result") else - error_code = response&.dig("ResponseStatus", "ErrorCode") + _, invalid_contact_status = client.ignorable_error?(response, mail) - contact_status = if client.ignorable_api_error_code?(error_code) - client.fetch_contact_status(mail.to.first) + if invalid_contact_status + raise Dolist::IgnorableError.new("DoList delivery error. contact unreachable: #{invalid_contact_status}") else - nil - end - if !client.ignorable_contact_status?(contact_status) fail "DoList delivery error. Body: #{response}" end end From 05cfb4055d86e195ddbea53fab8652abf5fade96 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 18 Apr 2023 13:09:22 +0200 Subject: [PATCH 18/75] fix(dossier): improuve dossier state display --- app/graphql/schema.graphql | 6 +++--- config/locales/models/dossier/en.yml | 4 ++-- config/locales/models/dossier/fr.yml | 6 +++--- spec/controllers/api/v2/graphql_controller_spec.rb | 4 ++-- spec/controllers/instructeurs/dossiers_controller_spec.rb | 8 ++++---- spec/helpers/dossier_helper_spec.rb | 6 +++--- spec/models/procedure_presentation_spec.rb | 2 +- spec/views/instructeur/dossiers/show.html.haml_spec.rb | 2 +- spec/views/users/dossiers/show/_header.html.haml_spec.rb | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 76d2b930f..e53f05039 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -1921,12 +1921,12 @@ enum DossierState { accepte """ - En construction + En construction """ en_construction """ - En instruction + En instruction """ en_instruction @@ -1936,7 +1936,7 @@ enum DossierState { refuse """ - Classé sans suite + Classé sans suite """ sans_suite } diff --git a/config/locales/models/dossier/en.yml b/config/locales/models/dossier/en.yml index 19fe8ff63..77edfb763 100644 --- a/config/locales/models/dossier/en.yml +++ b/config/locales/models/dossier/en.yml @@ -10,11 +10,11 @@ en: state: "State" dossier/state: &state brouillon: "Draft" - en_construction: "In progress" + en_construction: "In progress" en_instruction: "Processing" accepte: "Accepted" refuse: "Refused" - sans_suite: "No further action" + sans_suite: "No further action" traitement: state: "State" traitement/state: diff --git a/config/locales/models/dossier/fr.yml b/config/locales/models/dossier/fr.yml index 7f9f119c7..ba3638d05 100644 --- a/config/locales/models/dossier/fr.yml +++ b/config/locales/models/dossier/fr.yml @@ -14,11 +14,11 @@ fr: autorisation_donnees: Acceptation des CGU dossier/state: &state brouillon: "Brouillon" - en_construction: "En construction" - en_instruction: "En instruction" + en_construction: "En construction" + en_instruction: "En instruction" accepte: "Accepté" refuse: "Refusé" - sans_suite: "Classé sans suite" + sans_suite: "Classé sans suite" traitement: state: "État" traitement/state: diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index f91064be9..7d696111f 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -1072,7 +1072,7 @@ describe API::V2::GraphqlController do it "should fail" do expect(gql_errors).to eq(nil) expect(gql_data).to eq(dossierPasserEnInstruction: { - errors: [{ message: "Le dossier est déjà en instruction" }], + errors: [{ message: "Le dossier est déjà en instruction" }], dossier: nil }) end @@ -1289,7 +1289,7 @@ describe API::V2::GraphqlController do it "should fail" do expect(gql_errors).to eq(nil) expect(gql_data).to eq(dossierRefuser: { - errors: [{ message: "Le dossier est déjà classé sans suite" }], + errors: [{ message: "Le dossier est déjà classé sans suite" }], dossier: nil }) end diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 3c55f2b49..06f94d716 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -162,7 +162,7 @@ describe Instructeurs::DossiersController, type: :controller do it 'warns about the error' do expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_instruction)) expect(response).to have_http_status(:ok) - expect(response.body).to include('Le dossier est déjà en instruction.') + expect(response.body).to include('Le dossier est déjà en instruction.') end end @@ -175,7 +175,7 @@ describe Instructeurs::DossiersController, type: :controller do it 'warns about the error' do expect(response).to have_http_status(:ok) - expect(response.body).to include('Le dossier est en ce moment accepté : il n’est pas possible de le passer en instruction.') + expect(response.body).to include('Le dossier est en ce moment accepté : il n’est pas possible de le passer en instruction.') end end @@ -208,7 +208,7 @@ describe Instructeurs::DossiersController, type: :controller do it 'warns about the error' do expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction)) expect(response).to have_http_status(:ok) - expect(response.body).to include('Le dossier est déjà en construction.') + expect(response.body).to include('Le dossier est déjà en construction.') end end @@ -245,7 +245,7 @@ describe Instructeurs::DossiersController, type: :controller do it 'warns about the error' do expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_instruction)) expect(response).to have_http_status(:ok) - expect(response.body).to include('Le dossier est déjà en instruction.') + expect(response.body).to include('Le dossier est déjà en instruction.') end end diff --git a/spec/helpers/dossier_helper_spec.rb b/spec/helpers/dossier_helper_spec.rb index 72dcc91b6..d85eca805 100644 --- a/spec/helpers/dossier_helper_spec.rb +++ b/spec/helpers/dossier_helper_spec.rb @@ -141,7 +141,7 @@ RSpec.describe DossierHelper, type: :helper do it 'en_construction is En construction' do dossier.en_construction! - expect(subject).to eq('En construction') + expect(subject).to eq('En construction') end it 'accepte is traité' do @@ -151,12 +151,12 @@ RSpec.describe DossierHelper, type: :helper do it 'en_instruction is reçu' do dossier.en_instruction! - expect(subject).to eq('En instruction') + expect(subject).to eq('En instruction') end it 'sans_suite is traité' do dossier.sans_suite! - expect(subject).to eq('Classé sans suite') + expect(subject).to eq('Classé sans suite') end it 'refuse is traité' do diff --git a/spec/models/procedure_presentation_spec.rb b/spec/models/procedure_presentation_spec.rb index a4bd21d45..e0c898c64 100644 --- a/spec/models/procedure_presentation_spec.rb +++ b/spec/models/procedure_presentation_spec.rb @@ -789,7 +789,7 @@ describe ProcedurePresentation do let(:filters) { { "suivis" => [{ "table" => "self", "column" => "state", "value" => "en_construction" }] } } it 'should get i18n value' do - expect(subject).to eq("En construction") + expect(subject).to eq("En construction") end end end diff --git a/spec/views/instructeur/dossiers/show.html.haml_spec.rb b/spec/views/instructeur/dossiers/show.html.haml_spec.rb index b19e9cf9d..d521b2fda 100644 --- a/spec/views/instructeur/dossiers/show.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/show.html.haml_spec.rb @@ -20,7 +20,7 @@ describe 'instructeurs/dossiers/show.html.haml', type: :view do end it 'renders the correct dossier state' do - expect(subject).to have_text('en construction') + expect(subject).to have_text('en construction') end context 'with a motivation' do diff --git a/spec/views/users/dossiers/show/_header.html.haml_spec.rb b/spec/views/users/dossiers/show/_header.html.haml_spec.rb index bc931b537..f24cc1e14 100644 --- a/spec/views/users/dossiers/show/_header.html.haml_spec.rb +++ b/spec/views/users/dossiers/show/_header.html.haml_spec.rb @@ -11,7 +11,7 @@ describe 'users/dossiers/show/header.html.haml', type: :view do it 'affiche les informations du dossier' do expect(rendered).to have_text(dossier.procedure.libelle) expect(rendered).to have_text("Dossier nº #{dossier.id}") - expect(rendered).to have_text("en construction") + expect(rendered).to have_text("en construction") expect(rendered).to have_selector("nav.tabs") expect(rendered).to have_link("Résumé", href: dossier_path(dossier)) From 57e04667805dd9e1722b0657f6eb65f4b9cfcf40 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 09:23:27 +0200 Subject: [PATCH 19/75] chore(bundle): rubocop upgrades 1.36 => 1.50 --- Gemfile.lock | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4a5e78231..64ec024eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -473,9 +473,9 @@ GEM validate_url webfinger (>= 1.0.1) orm_adapter (0.5.0) - parallel (1.22.1) + parallel (1.23.0) parsby (1.1.1) - parser (3.1.2.1) + parser (3.2.2.0) ast (~> 2.4.1) pdf-core (0.9.0) pg (1.2.3) @@ -577,7 +577,7 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) redcarpet (3.6.0) - regexp_parser (2.7.0) + regexp_parser (2.8.0) request_store (1.5.0) rack (>= 1.4) responders (3.0.1) @@ -621,31 +621,33 @@ GEM rspec-support (3.10.2) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.36.0) + rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.21.0) - parser (>= 3.1.1.0) - rubocop-performance (1.9.2) - rubocop (>= 0.90.0, < 2.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.28.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.17.1) + rubocop (~> 1.41) + rubocop-performance (1.17.1) + rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.9.1) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.90.0, < 2.0) - rubocop-rspec (2.4.0) - rubocop (~> 1.0) - rubocop-ast (>= 1.1.0) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.20.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) ruby-graphviz (1.2.5) rexml - ruby-progressbar (1.11.0) + ruby-progressbar (1.13.0) ruby-vips (2.1.4) ffi (~> 1.12) ruby2_keywords (0.0.5) @@ -756,7 +758,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.7) - unicode-display_width (2.3.0) + unicode-display_width (2.4.2) uri_template (0.7.0) validate_email (0.1.6) activemodel (>= 3.0) From 3317f19323c7cb2cad58a742eb6173583aa6444f Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 09:26:10 +0200 Subject: [PATCH 20/75] chore(rubocop): fix Rails/ContentTag --- app/lib/redcarpet/bare_renderer.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/lib/redcarpet/bare_renderer.rb b/app/lib/redcarpet/bare_renderer.rb index 330195b1e..e672ee39a 100644 --- a/app/lib/redcarpet/bare_renderer.rb +++ b/app/lib/redcarpet/bare_renderer.rb @@ -4,7 +4,6 @@ module Redcarpet include ApplicationHelper # won't use rubocop tag method because it is missing output buffer - # rubocop:disable Rails/ContentTag def list(content, list_type) tag = list_type == :ordered ? :ol : :ul content_tag(tag, content, { class: @options[:class_names_map].fetch(:list) {} }, false) @@ -32,7 +31,5 @@ module Redcarpet link end end - - # rubocop:enable Rails/ContentTag end end From ca11ec838071d1c08f99252b39364c985d53ece7 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 09:26:28 +0200 Subject: [PATCH 21/75] chore(rubocop): fix Rails/OutputSafety --- app/models/types_de_champ/prefill_repetition_type_de_champ.rb | 2 +- app/models/types_de_champ/prefill_type_de_champ.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb index 61e3a15be..bbfa0293b 100644 --- a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb @@ -6,7 +6,7 @@ class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeCh [ I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html"), subchamps_all_possible_values - ].join("
").html_safe # rubocop:disable Rails/OutputSafety + ].join("
").html_safe end def example_value diff --git a/app/models/types_de_champ/prefill_type_de_champ.rb b/app/models/types_de_champ/prefill_type_de_champ.rb index b36139e65..52b3a7315 100644 --- a/app/models/types_de_champ/prefill_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_type_de_champ.rb @@ -83,6 +83,6 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator end def description - @description ||= I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html", default: nil)&.html_safe # rubocop:disable Rails/OutputSafety + @description ||= I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html", default: nil)&.html_safe end end From 923923f67da366d9c815a4c3926b90d3cc8ffbbd Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 10:46:36 +0200 Subject: [PATCH 22/75] chore(rubocop): enable most of new rails cops --- .rubocop.yml | 114 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index cb1b411a4..54f6fa312 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,6 +14,7 @@ AllCops: TargetRubyVersion: 3.1 DisabledByDefault: true SuggestExtensions: false + NewCops: enable Include: - "app/**/*.prawn" Exclude: @@ -683,9 +684,18 @@ Performance/UriDefaultParser: Rails: Enabled: true +Rails/ActionControllerFlashBeforeRender: + Enabled: true + +Rails/ActionControllerTestCase: + Enabled: true + Rails/ActionFilter: Enabled: true +Rails/ActionOrder: + Enabled: false + Rails/ActiveRecordAliases: Enabled: true @@ -695,18 +705,24 @@ Rails/ActiveRecordCallbacksOrder: Rails/ActiveSupportAliases: Enabled: true +Rails/ActiveSupportOnLoad: + Enabled: true + +Rails/AddColumnIndex: + Enabled: true + Rails/AfterCommitOverride: Enabled: true +Rails/ApplicationController: + Enabled: false + Rails/ApplicationJob: Enabled: true Rails/ApplicationRecord: Enabled: true -Rails/ApplicationController: - Enabled: false - Rails/AttributeDefaultBlockValue: Enabled: true @@ -716,6 +732,9 @@ Rails/Blank: Rails/BulkChangeTable: Enabled: false +Rails/CompactBlank: + Enabled: true + Rails/CreateTableWithTimestamps: Enabled: true Exclude: @@ -734,10 +753,28 @@ Rails/Delegate: Rails/DelegateAllowBlank: Enabled: true +Rails/DeprecatedActiveModelErrorsMethods: + Enabled: false # re-enable in rails 7 + +Rails/DotSeparatedKeys: + Enabled: true + +Rails/DuplicateAssociation: + Enabled: true + +Rails/DuplicateScope: + Enabled: true + +Rails/DurationArithmetic: + Enabled: true + Rails/DynamicFindBy: Enabled: true Exclude: - - "spec/system/**/*.rb" + - spec/system/**/*.rb + +Rails/EagerEvaluationLogMessage: + Enabled: true Rails/EnumUniqueness: Enabled: true @@ -748,6 +785,9 @@ Rails/EnvironmentComparison: Rails/Exit: Enabled: true +Rails/ExpandedDateRange: + Enabled: true + Rails/FilePath: Enabled: false @@ -760,6 +800,9 @@ Rails/FindById: Rails/FindEach: Enabled: true +Rails/FreezeTime: + Enabled: true + Rails/HasAndBelongsToMany: Enabled: false @@ -772,6 +815,18 @@ Rails/HttpPositionalArguments: Rails/HttpStatus: Enabled: false +Rails/I18nLazyLookup: + Enabled: true + +Rails/I18nLocaleAssignment: + Enabled: true + +Rails/I18nLocaleTexts: + Enabled: false + +Rails/IgnoredColumnsAssignment: + Enabled: true + Rails/Inquiry: Enabled: true @@ -787,6 +842,9 @@ Rails/MailerName: Rails/MatchRoute: Enabled: true +Rails/MigrationClassName: + Enabled: true + Rails/NegateInclude: Enabled: false @@ -823,9 +881,15 @@ Rails/ReadWriteAttribute: Rails/RedundantAllowNil: Enabled: false +Rails/RedundantPresenceValidationOnBelongsTo: + Enabled: true + Rails/RedundantReceiverInWithOptions: Enabled: true +Rails/RedundantTravelBack: + Enabled: true + Rails/RelativeDateConstant: Enabled: true @@ -838,9 +902,21 @@ Rails/RenderPlainText: Rails/RequestReferer: Enabled: true +Rails/ResponseParsedBody: + Enabled: true + Rails/ReversibleMigration: Enabled: false +Rails/RootJoinChain: + Enabled: true + +Rails/RootPathnameMethods: + Enabled: true + +Rails/RootPublicPath: + Enabled: true + Rails/SaveBang: Enabled: false @@ -856,9 +932,30 @@ Rails/SkipsModelValidations: Rails/SquishedSQLHeredocs: Enabled: true +Rails/StripHeredoc: + Enabled: true + +Rails/ThreeStateBooleanColumn: + Enabled: true + Rails/TimeZone: EnforcedStyle: strict +Rails/TimeZoneAssignment: + Enabled: true + +Rails/ToFormattedS: + Enabled: true + +Rails/ToSWithArgument: + Enabled: true + +Rails/TopLevelHashWithIndifferentAccess: + Enabled: true + +Rails/TransactionExitStatement: + Enabled: true + Rails/UniqBeforePluck: Enabled: true @@ -868,6 +965,9 @@ Rails/UniqueValidationWithoutIndex: Rails/UnknownEnv: Enabled: false +Rails/UnusedIgnoredColumns: + Enabled: true + Rails/Validation: Enabled: true @@ -877,9 +977,15 @@ Rails/WhereEquals: Rails/WhereExists: Enabled: true +Rails/WhereMissing: + Enabled: true + Rails/WhereNot: Enabled: true +Rails/WhereNotWithMultipleConditions: + Enabled: true + RSpec/Focus: Enabled: true From 6a3fd8fc4d68e49649576ed24b0ee1a5ce74d560 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 10:48:30 +0200 Subject: [PATCH 23/75] chore(rubocop): disable Rails/ThreeStateBooleanColumn for legacy migrations --- .rubocop.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 54f6fa312..df2111204 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -937,6 +937,13 @@ Rails/StripHeredoc: Rails/ThreeStateBooleanColumn: Enabled: true + Exclude: + - db/migrate/2019* + - db/migrate/2020* + - db/migrate/2021* + - db/migrate/2022* + - db/migrate/202301* + - db/migrate/202303* Rails/TimeZone: EnforcedStyle: strict From b273e7b67e805386ad4e099d0d340d8b91e356e3 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 10:55:16 +0200 Subject: [PATCH 24/75] chore(rubocop): fix Rails/RootPathnameMethods and assimiled cops --- .rubocop.yml | 6 ++++++ app/controllers/ping_controller.rb | 2 +- app/services/zxcvbn_service.rb | 2 +- config/initializers/core_ext.rb | 2 +- lib/tasks/deployment/20220922151100_populate_zones.rake | 2 +- spec/controllers/ping_controller_spec.rb | 7 ++++++- spec/controllers/webhook_controller_spec.rb | 4 ++-- spec/lib/tasks/graphql_spec.rb | 2 +- 8 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index df2111204..ca7a3de9a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1152,6 +1152,12 @@ Style/EvenOdd: Style/ExpandPathArguments: Enabled: true +Style/FileRead: + Enabled: true + +Style/FileWrite: + Enabled: true + Style/For: Enabled: true diff --git a/app/controllers/ping_controller.rb b/app/controllers/ping_controller.rb index 6fadefcbe..348a1e4fe 100644 --- a/app/controllers/ping_controller.rb +++ b/app/controllers/ping_controller.rb @@ -1,7 +1,7 @@ class PingController < ApplicationController def index Rails.logger.silence do - status_code = if File.file?(Rails.root.join("maintenance")) + status_code = if Rails.root.join("maintenance").file? # See https://cbonte.github.io/haproxy-dconv/2.0/configuration.html#4.2-http-check%20disable-on-404 :not_found elsif (ActiveRecord::Base.connection.execute('select 1 as test;').first['test'] == 1) diff --git a/app/services/zxcvbn_service.rb b/app/services/zxcvbn_service.rb index a6097ccc0..1930b66fe 100644 --- a/app/services/zxcvbn_service.rb +++ b/app/services/zxcvbn_service.rb @@ -19,7 +19,7 @@ class ZxcvbnService # # This is slow: loading and parsing the dictionary may take around 1s. def build_tester - dictionaries = YAML.safe_load(File.read(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml"))) + dictionaries = YAML.safe_load(Rails.root.join("config", "initializers", "zxcvbn_dictionnaries.yaml").read) tester = Zxcvbn::Tester.new tester.add_word_lists(dictionaries) diff --git a/config/initializers/core_ext.rb b/config/initializers/core_ext.rb index 99b320fcd..44ae562d2 100644 --- a/config/initializers/core_ext.rb +++ b/config/initializers/core_ext.rb @@ -1,3 +1,3 @@ -Dir[File.join(Rails.root, "lib", "core_ext", "*.rb")].each do |core_ext_file| +Dir[Rails.root.join("lib", "core_ext", "*.rb")].each do |core_ext_file| require core_ext_file end diff --git a/lib/tasks/deployment/20220922151100_populate_zones.rake b/lib/tasks/deployment/20220922151100_populate_zones.rake index 3b1b1ad12..3db312eb1 100644 --- a/lib/tasks/deployment/20220922151100_populate_zones.rake +++ b/lib/tasks/deployment/20220922151100_populate_zones.rake @@ -7,7 +7,7 @@ namespace :after_party do coll_label = collectivite.labels.find_or_initialize_by(designated_on: Date.parse('1977-07-30')) coll_label.update(name: 'Collectivité territoriale') - config = Psych.safe_load(File.read(Rails.root.join("config", "zones.yml"))) + config = Psych.safe_load(Rails.root.join("config", "zones.yml").read) config["ministeres"].each do |ministere| acronym = ministere.keys.first zone = Zone.find_or_create_by!(acronym: acronym) diff --git a/spec/controllers/ping_controller_spec.rb b/spec/controllers/ping_controller_spec.rb index c1c811c63..5b9e8c123 100644 --- a/spec/controllers/ping_controller_spec.rb +++ b/spec/controllers/ping_controller_spec.rb @@ -13,8 +13,13 @@ describe PingController, type: :controller do end context 'when a maintenance file is present' do + let(:filepath) { Rails.root.join('maintenance') } before do - allow(File).to receive(:file?).and_return(true) + filepath.write("") + end + + after do + filepath.delete end it 'tells HAProxy that the app is in maintenance, but will be available again soon' do diff --git a/spec/controllers/webhook_controller_spec.rb b/spec/controllers/webhook_controller_spec.rb index e95a00cfb..65931d4fd 100644 --- a/spec/controllers/webhook_controller_spec.rb +++ b/spec/controllers/webhook_controller_spec.rb @@ -6,7 +6,7 @@ describe WebhookController, type: :controller do describe '#helpscout_support_dev' do subject(:response) { post :helpscout_support_dev, params: payload } - let(:payload) { JSON.parse(File.read(Rails.root.join('spec', 'fixtures', 'files', 'helpscout', 'tagged-dev.json'))) } + let(:payload) { JSON.parse(Rails.root.join('spec', 'fixtures', 'files', 'helpscout', 'tagged-dev.json').read) } let(:webhook_url) { "https://notification_url" } it 'works' do allow(Rails.application.secrets).to receive(:dig).with(:mattermost, :support_webhook_url).and_return(webhook_url) @@ -68,7 +68,7 @@ describe WebhookController, type: :controller do describe '#sendinblue' do subject(:response) { post :sendinblue, params: payload } - let(:payload) { JSON.parse(File.read(Rails.root.join('spec', 'fixtures', 'files', 'sendinblue', 'incident.json'))) } + let(:payload) { JSON.parse(Rails.root.join('spec', 'fixtures', 'files', 'sendinblue', 'incident.json').read) } it 'sends notification to mattermost' do notification_url = "https://notification_url" diff --git a/spec/lib/tasks/graphql_spec.rb b/spec/lib/tasks/graphql_spec.rb index 24b772c9a..450a9d9e2 100644 --- a/spec/lib/tasks/graphql_spec.rb +++ b/spec/lib/tasks/graphql_spec.rb @@ -1,6 +1,6 @@ describe 'graphql' do let(:current_defn) { API::V2::Schema.to_definition } - let(:printout_defn) { File.read(Rails.root.join('app', 'graphql', 'schema.graphql')) } + let(:printout_defn) { Rails.root.join('app', 'graphql', 'schema.graphql').read } it "update the printed schema with `bin/rake graphql:schema:idl`" do result = GraphQL::SchemaComparator.compare(current_defn, printout_defn) From bd8e8633e75e9f885453a55b93a8c4ef81acacaf Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 10:59:21 +0200 Subject: [PATCH 25/75] chore(rubocop): fix Rails/CompactBlank --- app/controllers/support_controller.rb | 2 +- app/models/champs/multiple_drop_down_list_champ.rb | 2 +- app/models/etablissement.rb | 2 +- app/models/type_de_champ.rb | 2 +- .../types_de_champ/prefill_repetition_type_de_champ.rb | 2 +- app/views/dossiers/dossier_vide.pdf.prawn | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/support_controller.rb b/app/controllers/support_controller.rb index 526d17637..f741f85c4 100644 --- a/app/controllers/support_controller.rb +++ b/app/controllers/support_controller.rb @@ -73,7 +73,7 @@ class SupportController < ApplicationController [params[:tags], params[:type]].flatten.compact .map { |tag| tag.split(',') } .flatten - .reject(&:blank?).uniq + .compact_blank.uniq end def browser_name diff --git a/app/models/champs/multiple_drop_down_list_champ.rb b/app/models/champs/multiple_drop_down_list_champ.rb index 8c8999681..7b4d2d99a 100644 --- a/app/models/champs/multiple_drop_down_list_champ.rb +++ b/app/models/champs/multiple_drop_down_list_champ.rb @@ -118,7 +118,7 @@ class Champs::MultipleDropDownListChamp < Champ private def values_are_in_options - json = selected_options.reject(&:blank?) + json = selected_options.compact_blank return if json.empty? return if (json - enabled_non_empty_options).empty? diff --git a/app/models/etablissement.rb b/app/models/etablissement.rb index fb6c6fc4d..bf870e110 100644 --- a/app/models/etablissement.rb +++ b/app/models/etablissement.rb @@ -144,7 +144,7 @@ class Etablissement < ApplicationRecord "#{numero_voie} #{type_voie} #{nom_voie}", complement_adresse, "#{code_postal} #{localite}" - ].reject(&:blank?).join(', ').squeeze(' ') + ].compact_blank.join(', ').squeeze(' ') end def association? diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 3b00828c5..6fa285522 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -431,7 +431,7 @@ class TypeDeChamp < ApplicationRecord # then rails decided to add this blank ("") option when the select is required # so we revert this change def options_without_empty_value_when_mandatory(options) - mandatory? ? options.reject(&:blank?) : options + mandatory? ? options.compact_blank : options end def drop_down_list_options? diff --git a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb index bbfa0293b..7ff8076ed 100644 --- a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb @@ -18,7 +18,7 @@ class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeCh value.map.with_index do |repetition, index| PrefillRepetitionRow.new(champ, repetition, index, @revision).to_assignable_attributes - end.reject(&:blank?) + end.compact_blank end private diff --git a/app/views/dossiers/dossier_vide.pdf.prawn b/app/views/dossiers/dossier_vide.pdf.prawn index d6146b7b4..13224475e 100644 --- a/app/views/dossiers/dossier_vide.pdf.prawn +++ b/app/views/dossiers/dossier_vide.pdf.prawn @@ -154,7 +154,7 @@ def render_single_champ(pdf, champ) add_libelle(pdf, champ) add_optionnal_description(pdf, champ) add_explanation(pdf, 'Cochez la mention applicable, une seule valeur possible') - champ.options.reject(&:blank?).each do |option| + champ.options.compact_blank.each do |option| format_with_checkbox(pdf, option) end pdf.text "\n" @@ -162,15 +162,15 @@ def render_single_champ(pdf, champ) add_libelle(pdf, champ) add_optionnal_description(pdf, champ) add_explanation(pdf, 'Cochez la mention applicable, plusieurs valeurs possibles') - champ.options.reject(&:blank?).each do |option| + champ.options.compact_blank.each do |option| format_with_checkbox(pdf, option) end pdf.text "\n" when 'Champs::LinkedDropDownListChamp' add_libelle(pdf, champ) - champ.primary_options.reject(&:blank?).each do |o| + champ.primary_options.compact_blank.each do |o| format_with_checkbox(pdf, o) - champ.secondary_options[o].reject(&:blank?).each do |secondary_option| + champ.secondary_options[o].compact_blank.each do |secondary_option| format_with_checkbox(pdf, secondary_option, 15) end end From 999f06075bbfa7883fa4d3d8bf184f5185bc1845 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:00:18 +0200 Subject: [PATCH 26/75] chore(rubocop): fix Rails/ResponseParsedBody --- .../controllers/api/public/v1/dossiers_controller_spec.rb | 8 ++++---- spec/controllers/api/public/v1/stats_controller_spec.rb | 2 +- .../champs/piece_justificative_controller_spec.rb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/controllers/api/public/v1/dossiers_controller_spec.rb b/spec/controllers/api/public/v1/dossiers_controller_spec.rb index 09f85b55e..b6e40200f 100644 --- a/spec/controllers/api/public/v1/dossiers_controller_spec.rb +++ b/spec/controllers/api/public/v1/dossiers_controller_spec.rb @@ -35,9 +35,9 @@ RSpec.describe API::Public::V1::DossiersController, type: :controller do dossier = Dossier.last dossier_url = "http://test.host#{commencer_path(procedure.path, prefill_token: dossier.prefill_token)}" - expect(JSON.parse(response.body)["dossier_url"]).to eq(dossier_url) - expect(JSON.parse(response.body)["dossier_id"]).to eq(dossier.to_typed_id) - expect(JSON.parse(response.body)["dossier_number"]).to eq(dossier.id) + expect(response.parsed_body["dossier_url"]).to eq(dossier_url) + expect(response.parsed_body["dossier_id"]).to eq(dossier.to_typed_id) + expect(response.parsed_body["dossier_number"]).to eq(dossier.id) end context 'when prefill values are given' do @@ -141,7 +141,7 @@ RSpec.describe API::Public::V1::DossiersController, type: :controller do get :index, params: end - let(:body) { JSON.parse(response.body).map(&:deep_symbolize_keys) } + let(:body) { response.parsed_body.map(&:deep_symbolize_keys) } before { create_request } diff --git a/spec/controllers/api/public/v1/stats_controller_spec.rb b/spec/controllers/api/public/v1/stats_controller_spec.rb index 2ce5aa4fd..dac2fe7cf 100644 --- a/spec/controllers/api/public/v1/stats_controller_spec.rb +++ b/spec/controllers/api/public/v1/stats_controller_spec.rb @@ -14,7 +14,7 @@ RSpec.describe API::Public::V1::StatsController, type: :controller do it { expect(response).to be_successful } it { - expect(JSON.parse(response.body)).to match({ + expect(response.parsed_body).to match({ funnel: procedure.stats_dossiers_funnel.as_json, processed: procedure.stats_termines_states.as_json, processed_by_week: procedure.stats_termines_by_week.as_json, diff --git a/spec/controllers/champs/piece_justificative_controller_spec.rb b/spec/controllers/champs/piece_justificative_controller_spec.rb index aa983d745..49be46b79 100644 --- a/spec/controllers/champs/piece_justificative_controller_spec.rb +++ b/spec/controllers/champs/piece_justificative_controller_spec.rb @@ -65,7 +65,7 @@ describe Champs::PieceJustificativeController, type: :controller do subject expect(response.status).to eq(422) expect(response.header['Content-Type']).to include('application/json') - expect(JSON.parse(response.body)).to eq({ 'errors' => ['La pièce justificative n’est pas d’un type accepté'] }) + expect(response.parsed_body).to eq({ 'errors' => ['La pièce justificative n’est pas d’un type accepté'] }) end end From 160178b8f85a0260707aa2876896dd662f15b0d5 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:01:27 +0200 Subject: [PATCH 27/75] chore(rubocop): fix Rails/ExpandedDateRange --- app/jobs/cron/datagouv/account_by_month_job.rb | 2 +- app/jobs/cron/datagouv/administrateur_by_month_job.rb | 2 +- app/jobs/cron/datagouv/file_by_month_job.rb | 2 +- app/jobs/cron/datagouv/file_depose_by_month_job.rb | 2 +- app/jobs/cron/datagouv/instructeur_by_month_job.rb | 2 +- app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb | 2 +- app/jobs/cron/datagouv/procedure_by_month_job.rb | 2 +- app/jobs/cron/datagouv/procedure_closed_by_month_job.rb | 2 +- app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb | 2 +- .../datagouv/user_connected_with_france_connect_by_month_job.rb | 2 +- app/models/dossier.rb | 2 +- app/models/instructeur.rb | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/jobs/cron/datagouv/account_by_month_job.rb b/app/jobs/cron/datagouv/account_by_month_job.rb index 97f46bf5c..187da87db 100644 --- a/app/jobs/cron/datagouv/account_by_month_job.rb +++ b/app/jobs/cron/datagouv/account_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::AccountByMonthJob < Cron::CronJob end def data - User.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + User.where(created_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/administrateur_by_month_job.rb b/app/jobs/cron/datagouv/administrateur_by_month_job.rb index 346b9cf90..1391df690 100644 --- a/app/jobs/cron/datagouv/administrateur_by_month_job.rb +++ b/app/jobs/cron/datagouv/administrateur_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::AdministrateurByMonthJob < Cron::CronJob end def data - Administrateur.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Administrateur.where(created_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/file_by_month_job.rb b/app/jobs/cron/datagouv/file_by_month_job.rb index 723a48af3..f4375679a 100644 --- a/app/jobs/cron/datagouv/file_by_month_job.rb +++ b/app/jobs/cron/datagouv/file_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::FileByMonthJob < Cron::CronJob end def data - Dossier.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Dossier.where(created_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/file_depose_by_month_job.rb b/app/jobs/cron/datagouv/file_depose_by_month_job.rb index 1083d6c28..c7fd2d828 100644 --- a/app/jobs/cron/datagouv/file_depose_by_month_job.rb +++ b/app/jobs/cron/datagouv/file_depose_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::FileDeposeByMonthJob < Cron::CronJob end def data - Dossier.where(depose_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Dossier.where(depose_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/instructeur_by_month_job.rb b/app/jobs/cron/datagouv/instructeur_by_month_job.rb index cfa232c23..ea98d5dd9 100644 --- a/app/jobs/cron/datagouv/instructeur_by_month_job.rb +++ b/app/jobs/cron/datagouv/instructeur_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::InstructeurByMonthJob < Cron::CronJob end def data - Instructeur.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Instructeur.where(created_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb b/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb index 8691cb3fe..cbcb20e08 100644 --- a/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb +++ b/app/jobs/cron/datagouv/instructeur_connected_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::InstructeurConnectedByMonthJob < Cron::CronJob end def data - Instructeur.joins(:user).where(user: { last_sign_in_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month }).count + Instructeur.joins(:user).where(user: { last_sign_in_at: 1.month.ago.all_month }).count end end diff --git a/app/jobs/cron/datagouv/procedure_by_month_job.rb b/app/jobs/cron/datagouv/procedure_by_month_job.rb index 1327c4ff3..2a044cf43 100644 --- a/app/jobs/cron/datagouv/procedure_by_month_job.rb +++ b/app/jobs/cron/datagouv/procedure_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::ProcedureByMonthJob < Cron::CronJob end def data - Procedure.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Procedure.where(created_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb b/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb index 11ab5dbd8..315aa7474 100644 --- a/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb +++ b/app/jobs/cron/datagouv/procedure_closed_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::ProcedureClosedByMonthJob < Cron::CronJob end def data - Procedure.where(closed_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Procedure.where(closed_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb b/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb index d259a4af7..17bdccf63 100644 --- a/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb +++ b/app/jobs/cron/datagouv/procedure_deleted_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::ProcedureDeletedByMonthJob < Cron::CronJob end def data - Procedure.where(hidden_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).count + Procedure.where(hidden_at: 1.month.ago.all_month).count end end diff --git a/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb b/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb index 58d6ffdab..a4aea9be7 100644 --- a/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb +++ b/app/jobs/cron/datagouv/user_connected_with_france_connect_by_month_job.rb @@ -14,6 +14,6 @@ class Cron::Datagouv::UserConnectedWithFranceConnectByMonthJob < Cron::CronJob end def data - User.where(created_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month, loged_in_with_france_connect: "particulier").count + User.where(created_at: 1.month.ago.all_month, loged_in_with_france_connect: "particulier").count end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 5b6ddc16d..82489e54a 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -281,7 +281,7 @@ class Dossier < ApplicationRecord scope :processed_in_month, -> (date) do date = date.to_datetime state_termine - .where(processed_at: date.beginning_of_month..date.end_of_month) + .where(processed_at: date.all_month) end scope :ordered_for_export, -> { order(depose_at: 'asc') diff --git a/app/models/instructeur.rb b/app/models/instructeur.rb index ac2ee5a29..c6685b57c 100644 --- a/app/models/instructeur.rb +++ b/app/models/instructeur.rb @@ -208,7 +208,7 @@ class Instructeur < ApplicationRecord h = { nb_en_construction: groupe.dossiers.visible_by_administration.en_construction.count, nb_en_instruction: groupe.dossiers.visible_by_administration.en_instruction.count, - nb_accepted: Traitement.where(dossier: groupe.dossiers.accepte, processed_at: Time.zone.yesterday.beginning_of_day..Time.zone.yesterday.end_of_day).count, + nb_accepted: Traitement.where(dossier: groupe.dossiers.accepte, processed_at: Time.zone.yesterday.all_day).count, nb_notification: nb_notification } From 7aba5ccc3ecf1bfa78f53c9689492947adcdccb1 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:05:30 +0200 Subject: [PATCH 28/75] chore(rubocop): fix Rails/DurationArithmetic --- app/models/concerns/procedure_stats_concern.rb | 2 +- spec/factories/procedure.rb | 4 ++-- spec/models/procedure_spec.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/procedure_stats_concern.rb b/app/models/concerns/procedure_stats_concern.rb index 74f7dacd7..cfffaaa8c 100644 --- a/app/models/concerns/procedure_stats_concern.rb +++ b/app/models/concerns/procedure_stats_concern.rb @@ -108,7 +108,7 @@ module ProcedureStatsConcern end def last_considered_processed_at - (Time.zone.now - 1.month).end_of_month + (1.month.ago).end_of_month end def convert_seconds_in_days(seconds) diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index 263af1076..b2b330bec 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -318,7 +318,7 @@ FactoryBot.define do published aasm_state { :close } - published_at { Time.zone.now - 1.second } + published_at { 1.second.ago } closed_at { Time.zone.now } end @@ -326,7 +326,7 @@ FactoryBot.define do published aasm_state { :depubliee } - published_at { Time.zone.now - 1.second } + published_at { 1.second.ago } unpublished_at { Time.zone.now } end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index a6b8e0bbf..9a8260720 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -462,12 +462,12 @@ describe Procedure do end context "with token expired" do - let(:expiration_time) { (Time.zone.now - 1.day).to_i } + let(:expiration_time) { (1.day.ago).to_i } it { is_expected.to be_truthy } end context "with token not expired" do - let(:expiration_time) { (Time.zone.now + 1.day).to_i } + let(:expiration_time) { (1.day.from_now).to_i } it { is_expected.to be_falsey } end end From 8fa5e0dbb8c551ff642ce97fb40fc4b880815cd9 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:22:21 +0200 Subject: [PATCH 29/75] chore(rubocop): fix Rails/UnusedIgnoredColumns --- app/models/commentaire.rb | 1 - app/models/dossier.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index e18728e84..f5e3561e7 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -15,7 +15,6 @@ class Commentaire < ApplicationRecord include Discard::Model - self.ignored_columns = [:user_id] belongs_to :dossier, inverse_of: :commentaires, touch: true, optional: false belongs_to :instructeur, inverse_of: :commentaires, optional: true diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 82489e54a..446d6fda5 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -46,7 +46,6 @@ # user_id :integer # class Dossier < ApplicationRecord - self.ignored_columns = [:en_construction_conservation_extension] include DossierFilteringConcern include DossierPrefillableConcern include DossierRebaseConcern From e9649b667eaca5cbb1c331b7978dfc02516ac929 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:23:47 +0200 Subject: [PATCH 30/75] chore(rubocop): fix Rails/IgnoredColumnsAssignment --- app/models/concerns/blob_virus_scanner_concern.rb | 2 +- app/models/procedure.rb | 2 +- app/models/type_de_champ.rb | 2 +- .../20210429172327_rename_conservation_extension.rake | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/blob_virus_scanner_concern.rb b/app/models/concerns/blob_virus_scanner_concern.rb index 7c9a0d2c1..dd5fbdb51 100644 --- a/app/models/concerns/blob_virus_scanner_concern.rb +++ b/app/models/concerns/blob_virus_scanner_concern.rb @@ -2,7 +2,7 @@ module BlobVirusScannerConcern extend ActiveSupport::Concern included do - self.ignored_columns = [:lock_version] + self.ignored_columns += [:lock_version] before_create :set_pending end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index bb3360658..d38486a5e 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -65,7 +65,7 @@ class Procedure < ApplicationRecord include Discard::Model self.discard_column = :hidden_at - self.ignored_columns = [:direction, :durees_conservation_required, :cerfa_flag, :test_started_at, :lien_demarche] + self.ignored_columns += [:direction, :durees_conservation_required, :cerfa_flag, :test_started_at, :lien_demarche] default_scope -> { kept } diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 6fa285522..18720bb04 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -15,7 +15,7 @@ # stable_id :bigint # class TypeDeChamp < ApplicationRecord - self.ignored_columns = [:migrated_parent, :revision_id, :parent_id, :order_place] + self.ignored_columns += [:migrated_parent, :revision_id, :parent_id, :order_place] FILE_MAX_SIZE = 200.megabytes FEATURE_FLAGS = {} diff --git a/lib/tasks/deployment/20210429172327_rename_conservation_extension.rake b/lib/tasks/deployment/20210429172327_rename_conservation_extension.rake index 8d4bf2c05..4b18077cf 100644 --- a/lib/tasks/deployment/20210429172327_rename_conservation_extension.rake +++ b/lib/tasks/deployment/20210429172327_rename_conservation_extension.rake @@ -16,7 +16,7 @@ namespace :after_party do end progress.finish - Dossier.ignored_columns = ignored_columns + Dossier.ignored_columns = ignored_columns # rubocop:disable Rails/IgnoredColumnsAssignment dossiers_without_conservation_extension = Dossier.where(conservation_extension: nil) progress = ProgressReport.new((dossiers_without_conservation_extension.count.to_f / BATCH_SIZE).round) From 20f0b64736f958ae9a29a2834ae2cbae5a9021de Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:27:46 +0200 Subject: [PATCH 31/75] chore(rubocop): fix Rails/WhereNotWithMultipleConditions --- app/models/traitement.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/traitement.rb b/app/models/traitement.rb index 459eb3247..ebf3ee0d0 100644 --- a/app/models/traitement.rb +++ b/app/models/traitement.rb @@ -22,7 +22,8 @@ class Traitement < ApplicationRecord includes(:dossier) .termine .where(dossier: procedure.dossiers.visible_by_administration) - .where.not('dossiers.depose_at' => nil, processed_at: nil) + .where.not('dossiers.depose_at' => nil) + .where.not(processed_at: nil) .order(:processed_at) end end From 516ffc68ba0cc2f00d4534b70925ee1719da73c4 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:39:58 +0200 Subject: [PATCH 32/75] chore(rubocop): fix Rails/I18nLocaleAssignment --- spec/services/dossier_projection_service_spec.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/services/dossier_projection_service_spec.rb b/spec/services/dossier_projection_service_spec.rb index 6a09c4b26..ea72f3ba3 100644 --- a/spec/services/dossier_projection_service_spec.rb +++ b/spec/services/dossier_projection_service_spec.rb @@ -225,10 +225,12 @@ describe DossierProjectionService do let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :pays }]) } let(:dossier) { create(:dossier, procedure: procedure) } let(:column) { dossier.procedure.active_revision.types_de_champ_public.first.stable_id.to_s } - let!(:previous_locale) { I18n.locale } - before { I18n.locale = :fr } - after { I18n.locale = previous_locale } + around do |example| + I18n.with_locale(:fr) do + example.run + end + end context 'when external id is set' do before do From 6eec42135dc28bd869658182dc5b7553b15cbbfe Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:57:19 +0200 Subject: [PATCH 33/75] chore(rubocop): fix Rails/DotSeparatedKeys --- app/helpers/dossier_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index dd5b96b95..b94ac46e5 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -84,10 +84,10 @@ module DossierHelper def deletion_reason_badge(reason) if reason.present? - status_text = I18n.t(reason, scope: [:activerecord, :attributes, :deleted_dossier, :reason]) + status_text = I18n.t(reason, scope: 'activerecord.attributes.deleted_dossier.reason') status_class = reason.tr('_', '-') else - status_text = I18n.t(:unknown, scope: [:activerecord, :attributes, :deleted_dossier, :reason]) + status_text = I18n.t('activerecord.attributes.deleted_dossier.reason.unknown') status_class = 'unknown' end From f4ad5289cdf9f1206b46a133ecdc080c0a82158e Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 11:58:18 +0200 Subject: [PATCH 34/75] chore(rubocop): fix Rails/RedundantPresenceValidationOnBelongsTo --- app/models/avis.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/avis.rb b/app/models/avis.rb index 4525be4d6..fed16957d 100644 --- a/app/models/avis.rb +++ b/app/models/avis.rb @@ -42,7 +42,6 @@ class Avis < ApplicationRecord size: { less_than: FILE_MAX_SIZE } validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, allow_nil: true - validates :claimant, presence: true validates :question_answer, inclusion: { in: [true, false] }, on: :update, if: -> { question_label.present? } validates :piece_justificative_file, size: { less_than: FILE_MAX_SIZE } validates :introduction_file, size: { less_than: FILE_MAX_SIZE } From 1af404c5e769bdb9cdbe8d780b87bc7325117d6a Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Wed, 19 Apr 2023 12:03:08 +0200 Subject: [PATCH 35/75] chore(rubocop): fix Rails/WhereMissing --- app/models/dossier.rb | 2 +- lib/tasks/hotfix.rake | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 446d6fda5..4cd72becb 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -286,7 +286,7 @@ class Dossier < ApplicationRecord order(depose_at: 'asc') } scope :en_cours, -> { not_archived.state_en_construction_ou_instruction } - scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) } + scope :without_followers, -> { where.missing(:follows) } scope :with_followers, -> { left_outer_joins(:follows).where.not(follows: { id: nil }) } scope :with_champs, -> { includes(champs_public: [ diff --git a/lib/tasks/hotfix.rake b/lib/tasks/hotfix.rake index e03c0f35e..97e1625c9 100644 --- a/lib/tasks/hotfix.rake +++ b/lib/tasks/hotfix.rake @@ -3,10 +3,8 @@ namespace :hotfix do task dossiers_attestations: :environment do dossiers = Dossier .joins(procedure: :attestation_template) - .left_outer_joins(:attestation) - .where(attestation_templates: { activated: true }, - attestations: { id: nil }, - state: "accepte") + .where.missing(:attestation) + .where(attestation_templates: { activated: true }, state: "accepte") .where("dossiers.processed_at > '2022-01-24'") progress = ProgressReport.new(dossiers.count) From 8904516b857ecf8a2516c630fd423ea88aae845d Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 19 Apr 2023 13:00:37 +0200 Subject: [PATCH 36/75] chore(procedure): remove duplicate code --- app/controllers/administrateurs/procedures_controller.rb | 2 +- app/models/procedure.rb | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index 5e06ab767..07a7c1efd 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -219,7 +219,7 @@ module Administrateurs def restore procedure = current_administrateur.procedures.with_discarded.discarded.find(params[:id]) - procedure.restore_procedure(current_administrateur) + procedure.restore(current_administrateur) flash.notice = t('administrateurs.index.restored', procedure_id: procedure.id) redirect_to admin_procedures_path end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index bb3360658..3b7ffee8b 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -741,15 +741,6 @@ class Procedure < ApplicationRecord end end - def restore_procedure(author) - if discarded? - undiscard - self.dossiers.hidden_by_administration.each do |dossier| - dossier.restore(author) - end - end - end - def flipper_id "Procedure;#{id}" end From 95ab81fd1cf5eaa68f50d86d80aa08debead4faf Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 7 Apr 2023 14:31:06 +0200 Subject: [PATCH 37/75] feat(routing): use procedure.defaut_groupe_instructeur_id to find defaut_groupe_instructeur --- app/models/procedure.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index bb3360658..7eeb54453 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -208,7 +208,11 @@ class Procedure < ApplicationRecord has_one :refused_mail, class_name: "Mails::RefusedMail", dependent: :destroy has_one :without_continuation_mail, class_name: "Mails::WithoutContinuationMail", dependent: :destroy - has_one :defaut_groupe_instructeur, -> { active.order(id: :asc) }, class_name: 'GroupeInstructeur', inverse_of: false + belongs_to :defaut_groupe_instructeur, class_name: 'GroupeInstructeur', inverse_of: false, optional: true + + before_destroy prepend: true do |record| + record.update(defaut_groupe_instructeur_id: nil) + end has_one_attached :logo has_one_attached :notice From b38470368b425f835d930dd64aadfca186514abb Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 7 Apr 2023 15:44:23 +0200 Subject: [PATCH 38/75] feat(routing): can change defaut groupe --- .../stylesheets/routing_rules_component.scss | 13 +++++++++++++ .../routing_rules_component.html.haml | 9 +++++++++ .../administrateurs/routing_controller.rb | 11 ++++++++++- config/routes.rb | 1 + .../administrateurs/routing_controller_spec.rb | 18 ++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/routing_rules_component.scss b/app/assets/stylesheets/routing_rules_component.scss index 978b83352..ca5985cd3 100644 --- a/app/assets/stylesheets/routing_rules_component.scss +++ b/app/assets/stylesheets/routing_rules_component.scss @@ -29,6 +29,10 @@ .value { width: 200px; + + select { + width: 100%; + } } } @@ -56,4 +60,13 @@ border-color: $dark-red; } } + + .form.defaut-groupe { + padding: $default-spacer; + + label { + width: 600px; + font-size: 16px; + } + } } diff --git a/app/components/procedure/routing_rules_component/routing_rules_component.html.haml b/app/components/procedure/routing_rules_component/routing_rules_component.html.haml index e8532c4d7..197b1806e 100644 --- a/app/components/procedure/routing_rules_component/routing_rules_component.html.haml +++ b/app/components/procedure/routing_rules_component/routing_rules_component.html.haml @@ -28,5 +28,14 @@ %td.target= targeted_champ_tag(targeted_champ, row_index) %td.operator est égal à %td.value= value_tag(targeted_champ, value, row_index) + + = form_tag admin_procedure_update_defaut_groupe_instructeur_path, + class: 'form flex align-baseline defaut-groupe', + data: { controller: 'autosave' } do + = label_tag :defaut_groupe_instructeur_id, 'Et si aucune règle ne correspond, router vers :' + = select_tag :defaut_groupe_instructeur_id, + options_for_select(@groupe_instructeurs.pluck(:label, :id), selected: @revision.procedure.defaut_groupe_instructeur.id), + class: 'width-100' + - else .notice= t('.routing_rules_warning_html', path: champs_admin_procedure_path(@procedure_id)) diff --git a/app/controllers/administrateurs/routing_controller.rb b/app/controllers/administrateurs/routing_controller.rb index 999bf3b16..ec2f29e74 100644 --- a/app/controllers/administrateurs/routing_controller.rb +++ b/app/controllers/administrateurs/routing_controller.rb @@ -12,6 +12,11 @@ module Administrateurs groupe_instructeur.update!(routing_rule: ds_eq(left, right)) end + def update_defaut_groupe_instructeur + new_defaut = @procedure.groupe_instructeurs.find(defaut_groupe_instructeur_id) + @procedure.update!(defaut_groupe_instructeur: new_defaut) + end + private def targeted_champ_changed? @@ -34,8 +39,12 @@ module Administrateurs routing_params[:groupe_instructeur_id] end + def defaut_groupe_instructeur_id + routing_params[:defaut_groupe_instructeur_id] + end + def routing_params - params.permit(:targeted_champ, :value, :groupe_instructeur_id) + params.permit(:targeted_champ, :value, :groupe_instructeur_id, :defaut_groupe_instructeur_id) end end end diff --git a/config/routes.rb b/config/routes.rb index 532f81d05..106ca03a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -506,6 +506,7 @@ Rails.application.routes.draw do end patch :update, controller: 'routing', as: :routing_rules + patch :update_defaut_groupe_instructeur, controller: 'routing', as: :update_defaut_groupe_instructeur put 'clone' put 'archive' diff --git a/spec/controllers/administrateurs/routing_controller_spec.rb b/spec/controllers/administrateurs/routing_controller_spec.rb index f9106a4ca..6db618df0 100644 --- a/spec/controllers/administrateurs/routing_controller_spec.rb +++ b/spec/controllers/administrateurs/routing_controller_spec.rb @@ -49,4 +49,22 @@ describe Administrateurs::RoutingController, type: :controller do end end end + + describe "#update_defaut_groupe_instructeur" do + let(:procedure) { create(:procedure) } + let(:gi_2) { procedure.groupe_instructeurs.create(label: 'groupe 2') } + let(:params) do + { + procedure_id: procedure.id, + defaut_groupe_instructeur_id: gi_2.id + } + end + + before do + post :update_defaut_groupe_instructeur, params: params, format: :turbo_stream + procedure.reload + end + + it { expect(procedure.defaut_groupe_instructeur.id).to eq(gi_2.id) } + end end From 43b0eae3ee728dd8ca8263a4792954a30f0cc5f3 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 7 Apr 2023 16:48:24 +0200 Subject: [PATCH 39/75] refactor(ui): enough space for button --- app/assets/stylesheets/groupe_instructeur.scss | 9 +++++++-- .../administrateurs/groupe_instructeurs/_edit.html.haml | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/groupe_instructeur.scss b/app/assets/stylesheets/groupe_instructeur.scss index 419bf075f..a40243cd5 100644 --- a/app/assets/stylesheets/groupe_instructeur.scss +++ b/app/assets/stylesheets/groupe_instructeur.scss @@ -1,6 +1,11 @@ .groupe-instructeur { - .actions { - width: 200px; + .setup { text-align: center; + width: 100px; + } + + .actions { + text-align: center; + width: 250px; } } diff --git a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml index 9740a2b5a..320a51fb2 100644 --- a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml @@ -46,13 +46,13 @@ %tr // i18n-tasks-use t('.existing_groupe') %th{ colspan: 2 }= t(".existing_groupe", count: groupes_instructeurs.total_count) - %th + %th.actions = link_to "Exporter au format CSV", export_groupe_instructeurs_admin_procedure_groupe_instructeurs_path(procedure, format: :csv) %tbody - groupes_instructeurs.each do |group| %tr %td= group.label - %td.actions= link_to t('.set_up'), admin_procedure_groupe_instructeur_path(procedure, group) + %td.setup= link_to t('.set_up'), admin_procedure_groupe_instructeur_path(procedure, group) - if group.can_delete? %td.actions = link_to admin_procedure_groupe_instructeur_path(procedure, group), { method: :delete, class: 'button', data: { confirm: t('.group_management.delete_confirmation', group_name: group.label) }} do From b3ce3820a9ff090f4dcccc0e4c83d83c5e7e262d Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 7 Apr 2023 22:17:12 +0200 Subject: [PATCH 40/75] refactor(ui): use proper title --- .../routing_rules_component/routing_rules_component.html.haml | 3 +-- app/views/administrateurs/groupe_instructeurs/_edit.html.haml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/components/procedure/routing_rules_component/routing_rules_component.html.haml b/app/components/procedure/routing_rules_component/routing_rules_component.html.haml index 197b1806e..bad6ae5a3 100644 --- a/app/components/procedure/routing_rules_component/routing_rules_component.html.haml +++ b/app/components/procedure/routing_rules_component/routing_rules_component.html.haml @@ -1,6 +1,5 @@ .card#routing-rules - .card-title - = t('.apply_routing_rules') + %h2.card-title= t('.apply_routing_rules') - if can_route? .notice = t('.routing_rules_notice_html', path: champs_admin_procedure_path(@procedure_id)) diff --git a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml index 320a51fb2..b5ea5e640 100644 --- a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml @@ -11,8 +11,7 @@ = f.submit t('.button.rename'), class: 'button primary send' .card - .card-title - = t('.group_management.title') + %h2.card-title= t('.group_management.title') = form_for :groupe_instructeur, html: { class: 'form' } do |f| = f.label :label do From 86733248f35dee3b5d5789278b2f4058549fa1b5 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 7 Apr 2023 22:27:07 +0200 Subject: [PATCH 41/75] wording pour coller a Routage et Gestion des groupes --- .../routing_rules_component/routing_rules_component.fr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/procedure/routing_rules_component/routing_rules_component.fr.yml b/app/components/procedure/routing_rules_component/routing_rules_component.fr.yml index 8edf4d7d8..a7c9426d9 100644 --- a/app/components/procedure/routing_rules_component/routing_rules_component.fr.yml +++ b/app/components/procedure/routing_rules_component/routing_rules_component.fr.yml @@ -1,7 +1,7 @@ --- fr: select: Sélectionner - apply_routing_rules: Appliquer des règles de routage + apply_routing_rules: Règles de routage routing_rules_notice_html: |

Ajoutez des règles de routage à partir de champs « choix simple » créés dans le formulaire.

Les dossiers seront routées vers le premier groupe affiché dont la règle correspond.

From 3889bdac73fef6b7a54d3009b13f0d45272b93e4 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 7 Apr 2023 22:39:28 +0200 Subject: [PATCH 42/75] refactor(routing): remove unused routing_params --- .../administrateurs/routing_controller.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/administrateurs/routing_controller.rb b/app/controllers/administrateurs/routing_controller.rb index ec2f29e74..0e87ede8b 100644 --- a/app/controllers/administrateurs/routing_controller.rb +++ b/app/controllers/administrateurs/routing_controller.rb @@ -24,11 +24,11 @@ module Administrateurs end def targeted_champ - Logic.from_json(routing_params[:targeted_champ]) + Logic.from_json(params[:targeted_champ]) end def value - Logic.from_json(routing_params[:value]) + Logic.from_json(params[:value]) end def groupe_instructeur @@ -36,15 +36,11 @@ module Administrateurs end def groupe_instructeur_id - routing_params[:groupe_instructeur_id] + params[:groupe_instructeur_id] end def defaut_groupe_instructeur_id - routing_params[:defaut_groupe_instructeur_id] - end - - def routing_params - params.permit(:targeted_champ, :value, :groupe_instructeur_id, :defaut_groupe_instructeur_id) + params[:defaut_groupe_instructeur_id] end end end From 79131cc2949b478f15f7131920303e05c0d6bc39 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 18 Apr 2023 15:14:43 +0200 Subject: [PATCH 43/75] fix spec --- .../routing_rules_component.html.haml | 2 +- spec/factories/procedure.rb | 1 + spec/models/groupe_instructeur_spec.rb | 9 ++++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/components/procedure/routing_rules_component/routing_rules_component.html.haml b/app/components/procedure/routing_rules_component/routing_rules_component.html.haml index bad6ae5a3..9d0768031 100644 --- a/app/components/procedure/routing_rules_component/routing_rules_component.html.haml +++ b/app/components/procedure/routing_rules_component/routing_rules_component.html.haml @@ -28,7 +28,7 @@ %td.operator est égal à %td.value= value_tag(targeted_champ, value, row_index) - = form_tag admin_procedure_update_defaut_groupe_instructeur_path, + = form_tag admin_procedure_update_defaut_groupe_instructeur_path(@procedure_id), class: 'form flex align-baseline defaut-groupe', data: { controller: 'autosave' } do = label_tag :defaut_groupe_instructeur_id, 'Et si aucune règle ne correspond, router vers :' diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index 263af1076..54dec658b 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -27,6 +27,7 @@ FactoryBot.define do end after(:build) do |procedure, evaluator| + procedure.defaut_groupe_instructeur = procedure.groupe_instructeurs.first initial_revision = build(:procedure_revision, procedure: procedure, dossier_submitted_message: evaluator.dossier_submitted_message) if evaluator.types_de_champ_public.present? diff --git a/spec/models/groupe_instructeur_spec.rb b/spec/models/groupe_instructeur_spec.rb index ed1c65525..e50336410 100644 --- a/spec/models/groupe_instructeur_spec.rb +++ b/spec/models/groupe_instructeur_spec.rb @@ -100,9 +100,12 @@ describe GroupeInstructeur, type: :model do describe "active group validations" do context "there is at least one active groupe instructeur" do - let!(:gi_active) { create(:groupe_instructeur, procedure:, closed: false) } - let!(:gi_closed) { create(:groupe_instructeur, procedure:, closed: true) } - before { procedure.defaut_groupe_instructeur.destroy! } + let(:gi_active) { procedure.defaut_groupe_instructeur } + let(:gi_closed) { create(:groupe_instructeur, procedure:) } + before do + gi_active + gi_closed.update(closed: true) + end it "closed is valid when there is one other active groupe" do expect(gi_active).to be_valid From 09b05550ab06525f9198deca1ddfa52aa06d0079 Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Tue, 18 Apr 2023 16:40:53 +0200 Subject: [PATCH 44/75] feat(groupe instructeur): cannot delete defaut_groupe_instructeur --- .../administrateurs/groupe_instructeurs_controller.rb | 2 ++ .../administrateurs/groupe_instructeurs_controller_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb index 325c72c8a..a9bd23838 100644 --- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb +++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb @@ -68,6 +68,8 @@ module Administrateurs flash[:alert] = "Impossible de supprimer un groupe avec des dossiers. Il faut le réaffecter avant" elsif procedure.groupe_instructeurs.one? flash[:alert] = "Suppression impossible : il doit y avoir au moins un groupe instructeur sur chaque procédure" + elsif @groupe_instructeur.id == procedure.defaut_groupe_instructeur.id + flash[:alert] = "Suppression impossible : le groupe « #{@groupe_instructeur.label} » est le groupe par défaut." else @groupe_instructeur.destroy! if procedure.groupe_instructeurs.active.one? diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index 5cce2f10c..323665719 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -83,15 +83,15 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do } end - context 'with only one group' do + context 'default group' do before do delete_group gi_1_1 - delete_group gi_1_2 end it { expect(flash.alert).to be_present } + it { expect(flash.alert).to eq "Suppression impossible : le groupe « défaut » est le groupe par défaut." } it { expect(response).to redirect_to(admin_procedure_groupe_instructeurs_path(procedure)) } - it { expect(procedure.groupe_instructeurs.count).to eq(1) } + it { expect(procedure.groupe_instructeurs.count).to eq(2) } end context 'with many groups' do From aecd7559d240e3226eee46a2d0527a4a346c853d Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Tue, 18 Apr 2023 17:15:04 +0200 Subject: [PATCH 45/75] feat(groupe instructeur): cannot disable default groupe instructeur --- .../groupe_instructeurs_controller.rb | 9 ++++- .../groupe_instructeurs_controller_spec.rb | 33 +++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb index a9bd23838..036a3d105 100644 --- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb +++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb @@ -48,7 +48,10 @@ module Administrateurs def update @groupe_instructeur = groupe_instructeur - if @groupe_instructeur.update(groupe_instructeur_params) + if closed_params? && @groupe_instructeur.id == procedure.defaut_groupe_instructeur.id + redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur), + alert: "Il est impossible de désactiver le groupe d’instructeurs par défaut." + elsif @groupe_instructeur.update(groupe_instructeur_params) redirect_to admin_procedure_groupe_instructeur_path(procedure, groupe_instructeur), notice: "Le nom est à présent « #{@groupe_instructeur.label} »." else @@ -260,6 +263,10 @@ module Administrateurs private + def closed_params? + groupe_instructeur_params[:closed] == "1" + end + def procedure current_administrateur .procedures diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index 323665719..db1bd49e8 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -181,7 +181,7 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do describe '#update' do let(:new_name) { 'nouveau nom du groupe' } - let(:closed_value) { false } + let(:closed_value) { '0' } let!(:procedure_non_routee) { create(:procedure, :published, :for_individual, administrateurs: [admin]) } let!(:gi_1_1) { procedure_non_routee.defaut_groupe_instructeur } @@ -195,26 +195,33 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do gi_1_1.reload end - it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure_non_routee, gi_1_1)) } - it { expect(gi_1_1.label).to eq(new_name) } - it { expect(gi_1_1.closed).to eq(false) } - it { expect(flash.notice).to be_present } + it do + expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure_non_routee, gi_1_1)) + expect(gi_1_1.label).to eq(new_name) + expect(gi_1_1.closed).to eq(false) + expect(flash.notice).to be_present + end - context 'when we try do disable the only groupe instructeur' do - let(:closed_value) { true } + context 'when we try do disable the default groupe instructeur' do + let(:closed_value) { '1' } + let!(:gi_1_2) { procedure.groupe_instructeurs.create(label: 'groupe instructeur 2') } - it { expect(response).to render_template(:show) } - it { expect(gi_1_1.label).not_to eq(new_name) } - it { expect(gi_1_1.closed).to eq(false) } - it { expect(flash.alert).to eq(['Il doit y avoir au moins un groupe instructeur actif sur chaque démarche']) } + it do + expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure_non_routee, gi_1_1) + expect(gi_1_1.label).not_to eq(new_name) + expect(gi_1_1.closed).to eq(false) + expect(flash.alert).to eq('Il est impossible de désactiver le groupe d’instructeurs par défaut.') + end end context 'when the name is already taken' do let!(:gi_1_2) { procedure_non_routee.groupe_instructeurs.create(label: 'groupe instructeur 2') } let(:new_name) { gi_1_2.label } - it { expect(gi_1_1.label).not_to eq(new_name) } - it { expect(flash.alert).to eq(['Le libellé est déjà utilisé(e)']) } + it do + expect(gi_1_1.label).not_to eq(new_name) + expect(flash.alert).to eq(['Le libellé est déjà utilisé(e)']) + end end end From 9c6af921e5f641ebee7a141292267cb3ef0c57a2 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 19 Apr 2023 13:52:01 +0200 Subject: [PATCH 46/75] Use has_one defaut_procdre to avoid before destroy prepend --- app/models/groupe_instructeur.rb | 2 ++ app/models/procedure.rb | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index 19f20c77d..a3aca8266 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -21,6 +21,8 @@ class GroupeInstructeur < ApplicationRecord has_and_belongs_to_many :exports, dependent: :destroy has_and_belongs_to_many :bulk_messages, dependent: :destroy + has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur + validates :label, presence: true, allow_nil: false validates :label, uniqueness: { scope: :procedure } validates :closed, acceptance: { accept: [false] }, if: -> { closed_changed? && self.procedure.groupe_instructeurs.active.one? } diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 7eeb54453..73ab8fbc4 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -210,10 +210,6 @@ class Procedure < ApplicationRecord belongs_to :defaut_groupe_instructeur, class_name: 'GroupeInstructeur', inverse_of: false, optional: true - before_destroy prepend: true do |record| - record.update(defaut_groupe_instructeur_id: nil) - end - has_one_attached :logo has_one_attached :notice has_one_attached :deliberation From 63b7a8368910276485763b7680ee1c302a17bd50 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 19 Apr 2023 18:00:08 +0200 Subject: [PATCH 47/75] feat(graphql): log dossier and procedure id on dossier fetch endoint --- app/controllers/api/v2/dossiers_controller.rb | 7 +++++++ config/initializers/lograge.rb | 2 ++ 2 files changed, 9 insertions(+) diff --git a/app/controllers/api/v2/dossiers_controller.rb b/app/controllers/api/v2/dossiers_controller.rb index 9f5ef2033..16ae73c83 100644 --- a/app/controllers/api/v2/dossiers_controller.rb +++ b/app/controllers/api/v2/dossiers_controller.rb @@ -14,6 +14,13 @@ class API::V2::DossiersController < API::V2::BaseController private + def append_info_to_payload(payload) + super + if dossier.present? + payload.merge!(ds_dossier_id: dossier.id.to_s, ds_procedure_id: dossier.procedure.id.to_s) + end + end + def ensure_dossier_present if dossier.blank? head :unauthorized diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index b146985e7..422c6906d 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -18,6 +18,8 @@ Rails.application.configure do graphql_variables: event.payload[:graphql_variables], graphql_null_error: event.payload[:graphql_null_error], graphql_timeout_error: event.payload[:graphql_timeout_error], + ds_procedure_id: event.payload[:ds_procedure_id], + ds_dossier_id: event.payload[:ds_dossier_id], browser: event.payload[:browser], browser_version: event.payload[:browser_version], platform: event.payload[:platform], From 418d3b55d05f75f7471be683e204e3f5bb1675f8 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 19 Apr 2023 18:31:58 +0200 Subject: [PATCH 48/75] feat(stats): give access to stats on closed procedures --- app/controllers/api/public/v1/stats_controller.rb | 2 +- spec/controllers/api/public/v1/stats_controller_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/public/v1/stats_controller.rb b/app/controllers/api/public/v1/stats_controller.rb index 045850b2c..a05bf3fef 100644 --- a/app/controllers/api/public/v1/stats_controller.rb +++ b/app/controllers/api/public/v1/stats_controller.rb @@ -14,7 +14,7 @@ class API::Public::V1::StatsController < API::Public::V1::BaseController private def retrieve_procedure - @procedure = Procedure.publiees_ou_brouillons.opendata.find_by(id: params[:id]) + @procedure = Procedure.opendata.find_by(id: params[:id]) render_not_found("procedure", params[:id]) if @procedure.blank? end end diff --git a/spec/controllers/api/public/v1/stats_controller_spec.rb b/spec/controllers/api/public/v1/stats_controller_spec.rb index dac2fe7cf..f98b56d89 100644 --- a/spec/controllers/api/public/v1/stats_controller_spec.rb +++ b/spec/controllers/api/public/v1/stats_controller_spec.rb @@ -62,7 +62,7 @@ RSpec.describe API::Public::V1::StatsController, type: :controller do end context 'when the procedure is not publiee and not brouillon' do - it_behaves_like 'the procedure is not found' do + it_behaves_like 'the procedure is found' do let(:procedure) { create(:procedure, :closed) } end end From ce466d072a38694b6fe8266b978069492f57cb58 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 19 Apr 2023 18:29:18 +0200 Subject: [PATCH 49/75] feat(manager): display procedure state and unpublished date --- app/dashboards/procedure_dashboard.rb | 5 ++++- app/fields/procedure_state_field.rb | 7 +++++++ app/graphql/schema.graphql | 2 +- app/models/procedure.rb | 9 +++++++++ app/views/fields/procedure_state_field/_index.html.haml | 2 ++ app/views/fields/procedure_state_field/_show.html.haml | 2 ++ config/locales/models/procedure/fr.yml | 1 + 7 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 app/fields/procedure_state_field.rb create mode 100644 app/views/fields/procedure_state_field/_index.html.haml create mode 100644 app/views/fields/procedure_state_field/_show.html.haml diff --git a/app/dashboards/procedure_dashboard.rb b/app/dashboards/procedure_dashboard.rb index 4b0bae3c8..ee76168bd 100644 --- a/app/dashboards/procedure_dashboard.rb +++ b/app/dashboards/procedure_dashboard.rb @@ -11,6 +11,7 @@ class ProcedureDashboard < Administrate::BaseDashboard published_types_de_champ_public: TypesDeChampCollectionField, published_types_de_champ_private: TypesDeChampCollectionField, path: ProcedureLinkField, + aasm_state: ProcedureStateField, dossiers: Field::HasMany, administrateurs: Field::HasMany, id: Field::Number.with_options(searchable: true), @@ -56,7 +57,7 @@ class ProcedureDashboard < Administrate::BaseDashboard :service, :dossiers, :published_at, - :unpublished_at + :aasm_state ].freeze # SHOW_PAGE_ATTRIBUTES @@ -64,6 +65,7 @@ class ProcedureDashboard < Administrate::BaseDashboard SHOW_PAGE_ATTRIBUTES = [ :id, :path, + :aasm_state, :administrateurs, :libelle, :description, @@ -78,6 +80,7 @@ class ProcedureDashboard < Administrate::BaseDashboard :whitelisted_at, :hidden_at, :closed_at, + :unpublished_at, :published_types_de_champ_public, :published_types_de_champ_private, :for_individual, diff --git a/app/fields/procedure_state_field.rb b/app/fields/procedure_state_field.rb new file mode 100644 index 000000000..0f2f7752e --- /dev/null +++ b/app/fields/procedure_state_field.rb @@ -0,0 +1,7 @@ +require "administrate/field/base" + +class ProcedureStateField < Administrate::Field::String + def name + "Statut" + end +end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index e53f05039..ad44d1648 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -1070,7 +1070,7 @@ enum DemarcheState { close """ - Depubliee + Dépubliée """ depubliee diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 1b0713dea..955f3afbe 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -917,4 +917,13 @@ class Procedure < ApplicationRecord def stable_ids_used_by_routing_rules @stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact end + + # We need this to unfuck administrate + aasm + def self.human_attribute_name(attribute, options = {}) + if attribute == :aasm_state + 'Statut' + else + super + end + end end diff --git a/app/views/fields/procedure_state_field/_index.html.haml b/app/views/fields/procedure_state_field/_index.html.haml new file mode 100644 index 000000000..1f9061a3e --- /dev/null +++ b/app/views/fields/procedure_state_field/_index.html.haml @@ -0,0 +1,2 @@ +- if field.data.present? + = t field.data, scope: 'activerecord.attributes.procedure.aasm_state' diff --git a/app/views/fields/procedure_state_field/_show.html.haml b/app/views/fields/procedure_state_field/_show.html.haml new file mode 100644 index 000000000..1f9061a3e --- /dev/null +++ b/app/views/fields/procedure_state_field/_show.html.haml @@ -0,0 +1,2 @@ +- if field.data.present? + = t field.data, scope: 'activerecord.attributes.procedure.aasm_state' diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml index 53f378a3e..2c223d149 100644 --- a/config/locales/models/procedure/fr.yml +++ b/config/locales/models/procedure/fr.yml @@ -20,6 +20,7 @@ fr: publiee: Publiée close: Close hidden: Supprimée + depubliee: Dépubliée declarative_with_state/en_instruction: En instruction declarative_with_state/accepte: Accepté api_particulier_token: Jeton API Particulier From b9faa26560f4e0a36d841ef0e0fdeb357772ad9e Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 24 Mar 2023 10:31:01 +0100 Subject: [PATCH 50/75] =?UTF-8?q?amelioration(editeur-de-champs):=20permet?= =?UTF-8?q?=20de=20choisir=20un=20niveau=20de=20titre=20(de=201=20=C3=A0?= =?UTF-8?q?=203)=20pour=20les=20champs=20de=20type=20Champs::HeaderSection?= =?UTF-8?q?Champ.=20G=C3=A8re=20les=20erreurs=20dans=20le=20cas=20ou=20un?= =?UTF-8?q?=20administrateur=20supprimerait=20un=20HeaderSection=20qui=20c?= =?UTF-8?q?asserait=20l'ordre=20des=20titre=20[ex:=20titre=201,2,3.=20et?= =?UTF-8?q?=20l'admin=20supprime=20le=20titre=201=20et=20il=20reste=20que?= =?UTF-8?q?=202,3].?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stylesheets/conditions_component.scss | 12 +-- app/assets/stylesheets/errors_summary.scss | 12 +++ .../header_section_component.html.haml | 2 - .../champ_component/champ_component.html.haml | 5 ++ .../conditions_component.html.haml | 2 +- .../conditions_errors_component.html.haml | 2 +- .../types_de_champ_editor/errors_summary.rb | 26 ++++-- .../errors_summary/errors_summary.fr.yml | 10 ++- .../errors_summary/errors_summary.html.haml | 13 ++- .../header_section_component.rb | 42 ++++++++++ .../header_section_component.en.yml | 3 + .../header_section_component.fr.yml | 3 + .../header_section_component.html.haml | 5 ++ .../types_de_champ_controller.rb | 1 + app/models/champ.rb | 2 + app/models/procedure_revision.rb | 21 +++++ app/models/type_de_champ.rb | 32 ++++++- .../conditions/_update.turbo_stream.haml | 4 +- config/locales/models/type_de_champ/en.yml | 7 ++ config/locales/models/type_de_champ/fr.yml | 5 ++ .../header_section_component_spec.rb | 83 +++++++++++++++++++ .../conditions_errors_component_spec.rb | 6 +- spec/factories/champ.rb | 12 +++ spec/factories/type_de_champ.rb | 14 ++++ spec/models/procedure_revision_spec.rb | 18 ++++ spec/system/administrateurs/condition_spec.rb | 12 +-- .../administrateurs/types_de_champ_spec.rb | 27 ++++++ 27 files changed, 342 insertions(+), 39 deletions(-) create mode 100644 app/assets/stylesheets/errors_summary.scss delete mode 100644 app/components/editable_champ/header_section_component/header_section_component.html.haml create mode 100644 app/components/types_de_champ_editor/header_section_component.rb create mode 100644 app/components/types_de_champ_editor/header_section_component/header_section_component.en.yml create mode 100644 app/components/types_de_champ_editor/header_section_component/header_section_component.fr.yml create mode 100644 app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml create mode 100644 spec/components/header_section_component_spec.rb diff --git a/app/assets/stylesheets/conditions_component.scss b/app/assets/stylesheets/conditions_component.scss index 489630d35..b8a56d044 100644 --- a/app/assets/stylesheets/conditions_component.scss +++ b/app/assets/stylesheets/conditions_component.scss @@ -1,17 +1,7 @@ @import "colors"; @import "constants"; -.conditionnel { - - .condition-error { - background: $background-red; - margin: ($default-spacer) (-$default-spacer); - - ul { - padding: $default-spacer; - } - } - +form.form > .conditionnel { .condition-table { table-layout: fixed; diff --git a/app/assets/stylesheets/errors_summary.scss b/app/assets/stylesheets/errors_summary.scss new file mode 100644 index 000000000..54843e294 --- /dev/null +++ b/app/assets/stylesheets/errors_summary.scss @@ -0,0 +1,12 @@ +@import "colors"; +@import "constants"; + +.errors-summary { + background: $background-red; + margin: ($default-spacer) (-$default-spacer); + + ul { + padding: $default-spacer; + } +} + diff --git a/app/components/editable_champ/header_section_component/header_section_component.html.haml b/app/components/editable_champ/header_section_component/header_section_component.html.haml deleted file mode 100644 index aec7d22bd..000000000 --- a/app/components/editable_champ/header_section_component/header_section_component.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%h2.header-section{ class: @champ.dossier.auto_numbering_section_headers_for?(@champ) ? "header-section-counter" : nil } - = @champ.libelle diff --git a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml index e2f44f886..682cbd322 100644 --- a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml +++ b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml @@ -48,6 +48,11 @@ .cell.mt-1 = form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description) = form.text_area :description, class: 'small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :description) + - if type_de_champ.header_section? + .cell.mt-1 + = render TypesDeChampEditor::HeaderSectionComponent.new(form: form, tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ)) + + .flex.justify-start.mt-1 - if type_de_champ.drop_down_list? diff --git a/app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml b/app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml index 03be628eb..82e15064b 100644 --- a/app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml +++ b/app/components/types_de_champ_editor/conditions_component/conditions_component.html.haml @@ -1,4 +1,4 @@ -.flex.justify-start.section{ id: dom_id(@tdc.stable_self, :conditions) } +.flex.justify-start.section{ id: dom_id(@tdc.stable_self, :condition) } = form_tag admin_procedure_condition_path(@procedure_id, @tdc.stable_id), method: :patch, class: 'form width-100' do .conditionnel.mt-2.width-100 .flex diff --git a/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.html.haml b/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.html.haml index b0e3dbce7..d185f4d46 100644 --- a/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.html.haml +++ b/app/components/types_de_champ_editor/conditions_errors_component/conditions_errors_component.html.haml @@ -1,2 +1,2 @@ -.condition-error +.errors-summary = errors diff --git a/app/components/types_de_champ_editor/errors_summary.rb b/app/components/types_de_champ_editor/errors_summary.rb index aa71d9e67..b5e750e18 100644 --- a/app/components/types_de_champ_editor/errors_summary.rb +++ b/app/components/types_de_champ_editor/errors_summary.rb @@ -3,16 +3,32 @@ class TypesDeChampEditor::ErrorsSummary < ApplicationComponent @revision = revision end + def invalid? + @revision.invalid? + end + + def condition_errors? + @revision.errors.include?(:condition) + end + + def header_section_errors? + @revision.errors.include?(:header_section) + end + private - def error_message - @revision.errors + def errors_for(key) + @revision.errors.filter { _1.attribute == key } + end + + def error_message_for(key) + errors_for(key) .map { |error| error.options[:type_de_champ] } - .map { |tdc| tag.li(tdc_anchor(tdc)) } + .map { |tdc| tag.li(tdc_anchor(tdc, key)) } .then { |lis| tag.ul(lis.reduce(&:+)) } end - def tdc_anchor(tdc) - tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, :conditions)), data: { turbo: false }) + def tdc_anchor(tdc, key) + tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, key)), data: { turbo: false }) end end diff --git a/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml b/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml index 801c1d862..e22a7e225 100644 --- a/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml +++ b/app/components/types_de_champ_editor/errors_summary/errors_summary.fr.yml @@ -1,5 +1,9 @@ fr: - fix: - one: 'Corrigez le champ suivant :' - other: 'Corrigez les champs suivants :' + fix_conditional: + one: 'La logique conditionnelle du champ suivant est invalide, veuillez la corriger :' + other: 'La logique conditionnelle des champs suivants sont invalides, veuillez les corriger :' + + fix_header_section: + one: 'Le titre de section suivant est invalide, veuillez le corriger :' + other: 'Les titres de section suivants sont invalides, veuillez les corriger :' diff --git a/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml b/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml index e43e9f61c..b9dff099f 100644 --- a/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml +++ b/app/components/types_de_champ_editor/errors_summary/errors_summary.html.haml @@ -1,7 +1,12 @@ #errors-summary - - if @revision.invalid? + - if invalid? .card.warning - .card-title La logique conditionnelle est devenue invalide + .card-title Le formulaire contient des erreurs - %p.mb-2= t('.fix', count: @revision.errors.count) - = error_message + - if condition_errors? + %p.mb-2= t('.fix_conditional', count: errors_for(:condition).size) + = error_message_for(:condition) + + - if header_section_errors? + %p.mb-2= t('.fix_header_section', count: errors_for(:header_section).size) + = error_message_for(:header_section) diff --git a/app/components/types_de_champ_editor/header_section_component.rb b/app/components/types_de_champ_editor/header_section_component.rb new file mode 100644 index 000000000..51fbac301 --- /dev/null +++ b/app/components/types_de_champ_editor/header_section_component.rb @@ -0,0 +1,42 @@ +class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent + MAX_LEVEL = 3 + + def initialize(form:, tdc:, upper_tdcs:) + @form = form + @tdc = tdc + @upper_tdcs = upper_tdcs + end + + def header_section_options_for_select + closest_level = @tdc.previous_section_level(@upper_tdcs) + next_level = [MAX_LEVEL, closest_level + 1].min + + available_levels = (1..next_level).map(&method(:option_for_level)) + disabled_levels = errors? ? (next_level + 1..MAX_LEVEL).map(&method(:option_for_level)) : [] + options_for_select( + available_levels + disabled_levels, + disabled: disabled_levels.map(&:second), + selected: @tdc.header_section_level_value + ) + end + + def errors + @tdc.check_nesting(@upper_tdcs) + end + + private + + def option_for_level(level) + [translate(".select_option", level: level), level] + end + + def errors? + !errors.empty? + end + + def to_html_list(messages) + messages + .map { |message| tag.li(message) } + .then { |lis| tag.ul(lis.reduce(&:+)) } + end +end diff --git a/app/components/types_de_champ_editor/header_section_component/header_section_component.en.yml b/app/components/types_de_champ_editor/header_section_component/header_section_component.en.yml new file mode 100644 index 000000000..4ee130733 --- /dev/null +++ b/app/components/types_de_champ_editor/header_section_component/header_section_component.en.yml @@ -0,0 +1,3 @@ +--- +en: + select_option: "Header section %{level}" diff --git a/app/components/types_de_champ_editor/header_section_component/header_section_component.fr.yml b/app/components/types_de_champ_editor/header_section_component/header_section_component.fr.yml new file mode 100644 index 000000000..16e35d8a8 --- /dev/null +++ b/app/components/types_de_champ_editor/header_section_component/header_section_component.fr.yml @@ -0,0 +1,3 @@ +--- +fr: + select_option: "Titre de niveau %{level}" diff --git a/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml b/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml new file mode 100644 index 000000000..76b3b10e4 --- /dev/null +++ b/app/components/types_de_champ_editor/header_section_component/header_section_component.html.haml @@ -0,0 +1,5 @@ +%div{ id: dom_id(@tdc.stable_self, :header_section) } +- if errors? + .errors-summary= to_html_list(errors) += @form.label :header_section_level, "Niveau du titre", for: dom_id(@tdc, :header_section_level) += @form.select :header_section_level, header_section_options_for_select, {}, id: dom_id(@tdc, :header_section_level) diff --git a/app/controllers/administrateurs/types_de_champ_controller.rb b/app/controllers/administrateurs/types_de_champ_controller.rb index e6cbcf8ef..48dbf9d72 100644 --- a/app/controllers/administrateurs/types_de_champ_controller.rb +++ b/app/controllers/administrateurs/types_de_champ_controller.rb @@ -128,6 +128,7 @@ module Administrateurs :drop_down_secondary_description, :collapsible_explanation_enabled, :collapsible_explanation_text, + :header_section_level editable_options: [ :cadastres, :unesco, diff --git a/app/models/champ.rb b/app/models/champ.rb index 9b7a1c727..53519d25d 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -48,6 +48,8 @@ class Champ < ApplicationRecord :drop_down_secondary_description, :collapsible_explanation_enabled?, :collapsible_explanation_text, + :header_section_level, + :current_section_level, :exclude_from_export?, :exclude_from_view?, :repetition?, diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index e90cf75a8..0ab4eabf8 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -31,6 +31,7 @@ class ProcedureRevision < ApplicationRecord scope :ordered, -> { order(:created_at) } validate :conditions_are_valid? + validate :header_sections_are_valid? delegate :path, to: :procedure, prefix: true @@ -401,4 +402,24 @@ class ProcedureRevision < ApplicationRecord .filter { |_tdc, errors| errors.present? } .each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) } end + + def header_sections_are_valid? + public_tdcs = types_de_champ_public.to_a + + root_tdcs_errors = errors_for_header_sections_order(public_tdcs) + repetition_tdcs_errors = public_tdcs + .filter_map { _1.repetition? ? children_of(_1) : nil } + .map { errors_for_header_sections_order(_1) } + + repetition_tdcs_errors + root_tdcs_errors + end + + def errors_for_header_sections_order(tdcs) + tdcs + .map.with_index + .filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil } + .map { |tdc, i| [tdc, tdc.check_nesting(tdcs.take(i))] } + .filter { |_tdc, errors| errors.present? } + .each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) } + end end diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 18720bb04..0e8b4c19e 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -119,7 +119,8 @@ class TypeDeChamp < ApplicationRecord :drop_down_secondary_description, :drop_down_other, :collapsible_explanation_enabled, - :collapsible_explanation_text + :collapsible_explanation_text, + :header_section_level has_many :revision_types_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ has_one :revision_type_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', inverse_of: false @@ -411,6 +412,35 @@ class TypeDeChamp < ApplicationRecord self.drop_down_options = parse_drop_down_list_value(value) end + def header_section_level_value + header_section_level.presence || 1 + end + + def previous_section_level(upper_tdcs) + previous_header_section = upper_tdcs.reverse.find(&:header_section?) + + return 0 if !previous_header_section + previous_header_section.header_section_level_value.to_i + end + + def check_nesting(upper_tdcs) + errs = [] + previous_level = previous_section_level(upper_tdcs) + + current_level = header_section_level_value.to_i + difference = current_level - previous_level + if current_level > previous_level && difference != 1 + errs << I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1) + end + errs + end + + def current_section_level + tdcs = private? ? revision.type_champs_private.to_a : revision.types_de_champ_public.to_a + + previous_section_level(tdcs.take(tdcs.find_index(self))) + end + def self.options_for_select?(type_champs) [ TypeDeChamp.type_champs.fetch(:departements), diff --git a/app/views/administrateurs/conditions/_update.turbo_stream.haml b/app/views/administrateurs/conditions/_update.turbo_stream.haml index 9f4430bb5..3fba14ec8 100644 --- a/app/views/administrateurs/conditions/_update.turbo_stream.haml +++ b/app/views/administrateurs/conditions/_update.turbo_stream.haml @@ -9,7 +9,7 @@ - rendered = render @condition_component - if rendered.present? - = turbo_stream.replace dom_id(@tdc.stable_self, :conditions) do + = turbo_stream.replace dom_id(@tdc.stable_self, :condition) do - rendered - else - = turbo_stream.remove dom_id(@tdc.stable_self, :conditions) + = turbo_stream.remove dom_id(@tdc.stable_self, :condition) diff --git a/config/locales/models/type_de_champ/en.yml b/config/locales/models/type_de_champ/en.yml index d2917ac8b..379a7928b 100644 --- a/config/locales/models/type_de_champ/en.yml +++ b/config/locales/models/type_de_champ/en.yml @@ -50,3 +50,10 @@ 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" + errors: + type_de_champ: + attributes: + header_section_level: + gap_error: "An header section with %{level} is missing." + + diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml index 9a23b1866..989cba473 100644 --- a/config/locales/models/type_de_champ/fr.yml +++ b/config/locales/models/type_de_champ/fr.yml @@ -50,3 +50,8 @@ 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" + errors: + type_de_champ: + attributes: + header_section_level: + gap_error: "Un titre de section avec le niveau %{level} est manquant." diff --git a/spec/components/header_section_component_spec.rb b/spec/components/header_section_component_spec.rb new file mode 100644 index 000000000..5a3be5912 --- /dev/null +++ b/spec/components/header_section_component_spec.rb @@ -0,0 +1,83 @@ +RSpec.describe TypesDeChampEditor::HeaderSectionComponent, type: :component do + include ActionView::Context + include ActionView::Helpers::FormHelper + include ActionView::Helpers::FormOptionsHelper + + let(:component) do + cmp = nil + form_for(tdc, url: '/') do |form| + cmp = described_class.new(form: form, tdc: tdc, upper_tdcs: upper_tdcs) + end + cmp + end + subject { render_inline(component).to_html } + + describe 'header_section_options_for_select' do + context 'without upper tdc' do + let(:tdc) { header.type_de_champ } + let(:header) { build(:champ_header_section) } + let(:upper_tdcs) { [] } + + it 'allows up to level 1 header section' do + expect(subject).to have_selector("option", count: 1) + end + end + + context 'with upper tdc of level 1' do + let(:tdc) { header.type_de_champ } + let(:header) { build(:champ_header_section_level_1) } + let(:upper_tdcs) { [build(:champ_header_section_level_1).type_de_champ] } + + it 'allows up to level 2 header section' do + expect(subject).to have_selector("option", count: 2) + end + end + + context 'with upper tdc of level 2' do + let(:tdc) { header.type_de_champ } + let(:header) { build(:champ_header_section_level_1) } + let(:upper_tdcs) { [build(:champ_header_section_level_1), build(:champ_header_section_level_2)].map(&:type_de_champ) } + + it 'allows up to level 3 header section' do + expect(subject).to have_selector("option", count: 3) + end + end + + context 'with upper tdc of level 3' do + let(:tdc) { header.type_de_champ } + let(:header) { build(:champ_header_section_level_1) } + let(:upper_tdcs) do + [ + build(:champ_header_section_level_1), + build(:champ_header_section_level_2), + build(:champ_header_section_level_3) + ].map(&:type_de_champ) + end + + it 'reaches limit of at most 3 section level' do + expect(subject).to have_selector("option", count: 3) + end + end + + context 'with error' do + let(:tdc) { header.type_de_champ } + let(:header) { build(:champ_header_section_level_2) } + let(:upper_tdcs) { [] } + + it 'includes disabled levels' do + expect(subject).to have_selector("option", count: 3) + expect(subject).to have_selector("option[disabled]", count: 2) + end + end + end + + describe 'errors' do + let(:tdc) { header.type_de_champ } + let(:header) { build(:champ_header_section_level_2) } + let(:upper_tdcs) { [] } + + it 'returns errors' do + expect(subject).to have_selector('.errors-summary') + end + end +end diff --git a/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb b/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb index bdc5a83c3..c76e53dc5 100644 --- a/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb +++ b/spec/components/types_de_champ_editor/conditions_errors_component_spec.rb @@ -8,7 +8,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do before { render_inline(described_class.new(conditions: conditions, upper_tdcs: upper_tdcs)) } context 'when there are no condition' do - it { expect(page).to have_no_css('.condition-error') } + it { expect(page).to have_no_css('.errors-summary') } end context 'when the targeted_champ is not available' do @@ -16,7 +16,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] } it do - expect(page).to have_css('.condition-error') + expect(page).to have_css('.errors-summary') expect(page).to have_content("Un champ cible n'est plus disponible") end end @@ -27,7 +27,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] } it do - expect(page).to have_css('.condition-error') + expect(page).to have_css('.errors-summary') expect(page).to have_content("Le champ « #{tdc.libelle} » est de type « adresse » et ne peut pas être utilisé comme champ cible.") end end diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb index ca13c4325..350f2814a 100644 --- a/spec/factories/champ.rb +++ b/spec/factories/champ.rb @@ -137,6 +137,18 @@ FactoryBot.define do type_de_champ { association :type_de_champ_header_section, procedure: dossier.procedure } value { 'une section' } end + factory :champ_header_section_level_1, class: 'Champs::HeaderSectionChamp' do + type_de_champ { association :type_de_champ_header_section_level_1, procedure: dossier.procedure } + value { 'une section' } + end + factory :champ_header_section_level_2, class: 'Champs::HeaderSectionChamp' do + type_de_champ { association :type_de_champ_header_section_level_2, procedure: dossier.procedure } + value { 'une section' } + end + factory :champ_header_section_level_3, class: 'Champs::HeaderSectionChamp' do + type_de_champ { association :type_de_champ_header_section_level_3, procedure: dossier.procedure } + value { 'une section' } + end factory :champ_explication, class: 'Champs::ExplicationChamp' do type_de_champ { association :type_de_champ_explication, procedure: dossier.procedure } diff --git a/spec/factories/type_de_champ.rb b/spec/factories/type_de_champ.rb index f538ef10f..bb9587a37 100644 --- a/spec/factories/type_de_champ.rb +++ b/spec/factories/type_de_champ.rb @@ -114,6 +114,20 @@ FactoryBot.define do factory :type_de_champ_header_section do type_champ { TypeDeChamp.type_champs.fetch(:header_section) } end + + factory :type_de_champ_header_section_level_1 do + type_champ { TypeDeChamp.type_champs.fetch(:header_section) } + header_section_level { 1 } + end + factory :type_de_champ_header_section_level_2 do + type_champ { TypeDeChamp.type_champs.fetch(:header_section) } + header_section_level { 2 } + end + factory :type_de_champ_header_section_level_3 do + type_champ { TypeDeChamp.type_champs.fetch(:header_section) } + header_section_level { 3 } + end + factory :type_de_champ_explication do type_champ { TypeDeChamp.type_champs.fetch(:explication) } end diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb index 81a9825c4..b115ad62f 100644 --- a/spec/models/procedure_revision_spec.rb +++ b/spec/models/procedure_revision_spec.rb @@ -890,6 +890,24 @@ describe ProcedureRevision do end end + describe 'header_sections_are_valid' do + let(:procedure) do + create(:procedure).tap do |p| + p.draft_revision.add_type_de_champ(type_champ: :header_section, libelle: 'hs', header_section_level: '2') + end + end + let(:draft_revision) { procedure.draft_revision } + + subject do + draft_revision.save + draft_revision.errors + end + + it 'find error' do + expect(subject.errors).not_to be_empty + end + end + describe "#dependent_conditions" do include Logic diff --git a/spec/system/administrateurs/condition_spec.rb b/spec/system/administrateurs/condition_spec.rb index 698c6d03e..beac28897 100644 --- a/spec/system/administrateurs/condition_spec.rb +++ b/spec/system/administrateurs/condition_spec.rb @@ -87,19 +87,19 @@ describe 'As an administrateur I can edit types de champ condition', js: true do end scenario "changing target champ to a not managed type" do - expect(page).to have_no_selector('.condition-error') + expect(page).to have_no_selector('.errors-summary') within '.type-de-champ:nth-child(1)' do select('Départements', from: 'Type de champ') end within '.type-de-champ:nth-child(2)' do - expect(page).to have_selector('.condition-error') + expect(page).to have_selector('.errors-summary') end end scenario "moving a target champ below the condition" do - expect(page).to have_no_selector('.condition-error') + expect(page).to have_no_selector('.errors-summary') within '.type-de-champ:nth-child(1)' do click_on 'Déplacer le champ vers le bas' @@ -107,12 +107,12 @@ describe 'As an administrateur I can edit types de champ condition', js: true do # the now first champ has an error within '.type-de-champ:nth-child(1)' do - expect(page).to have_selector('.condition-error') + expect(page).to have_selector('.errors-summary') end end scenario "moving the condition champ above the target" do - expect(page).to have_no_selector('.condition-error') + expect(page).to have_no_selector('.errors-summary') within '.type-de-champ:nth-child(2)' do click_on 'Déplacer le champ vers le haut' @@ -120,7 +120,7 @@ describe 'As an administrateur I can edit types de champ condition', js: true do # the now first champ has an error within '.type-de-champ:nth-child(1)' do - expect(page).to have_selector('.condition-error') + expect(page).to have_selector('.errors-summary') end end end diff --git a/spec/system/administrateurs/types_de_champ_spec.rb b/spec/system/administrateurs/types_de_champ_spec.rb index 835f62a3e..1ab7e42c8 100644 --- a/spec/system/administrateurs/types_de_champ_spec.rb +++ b/spec/system/administrateurs/types_de_champ_spec.rb @@ -1,4 +1,6 @@ describe 'As an administrateur I can edit types de champ', js: true do + include ActionView::RecordIdentifier + let(:administrateur) { procedure.administrateurs.first } let(:estimated_duration_visible) { true } let(:procedure) { create(:procedure, estimated_duration_visible:) } @@ -192,4 +194,29 @@ describe 'As an administrateur I can edit types de champ', js: true do expect(page).not_to have_content('Durée de remplissage estimée') end end + + context 'header section' do + scenario 'invalid order, it pops up errors summary' do + add_champ + wait_until { procedure.active_revision.types_de_champ_public.size == 1 } + first_header = procedure.active_revision.types_de_champ_public.first + select('Titre de section', from: 'Type de champ') + select('Titre de niveau 1', from: dom_id(first_header, :header_section_level)) + + add_champ + wait_until { procedure.reload.active_revision.types_de_champ_public.count == 2 } + second_header = procedure.active_revision.types_de_champ_public.last + select('Titre de section', from: dom_id(second_header, :type_champ)) + select('Titre de niveau 2', from: dom_id(second_header, :header_section_level)) + + within(".types-de-champ-block li:first-child") do + page.accept_alert do + click_on 'Supprimer' + end + end + + expect(page).to have_content("Le formulaire contient des erreurs") + expect(page).to have_content("Le titre de section suivant est invalide, veuillez le corriger :") + end + end end From ad77b9321b877a75ae7059154a9216045b3ed65d Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 24 Mar 2023 11:08:31 +0100 Subject: [PATCH 51/75] =?UTF-8?q?amelioration(formulaire-usager):=20groupe?= =?UTF-8?q?s=20les=20champs=20suivant=20un=20titre=20de=20section=20dans?= =?UTF-8?q?=20une=20balise=20html=20de=20type=20
=20de=20sorte=20?= =?UTF-8?q?a=20am=C3=A9liorer=20l'accessibilite=20de=20la=20plateforme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/forms.scss | 8 - app/assets/stylesheets/sections.scss | 55 ++++++- app/components/champs/treeable.rb | 42 ++++++ .../champs_subtree_component.rb | 41 +++++ .../champs_subtree_component.html.haml | 17 +++ .../editable_champ/champs_tree_component.rb | 13 ++ .../champs_tree_component.html.haml | 1 + .../fields_for_champ_component.rb | 5 + .../fields_for_champ_component.html.haml | 2 + .../header_section_component.rb | 22 ++- .../header_section_component.html.haml | 2 + .../repetition_row_component.html.haml | 4 +- .../header_section_component.rb | 4 +- app/controllers/root_controller.rb | 7 +- app/models/champ.rb | 2 +- app/models/procedure_revision.rb | 2 +- app/models/type_de_champ.rb | 2 +- app/views/shared/dossiers/_edit.html.haml | 6 +- .../dossiers/_edit_annotations.html.haml | 4 +- .../champs_tree_component_spec.rb | 142 ++++++++++++++++++ .../administrateurs/types_de_champ_spec.rb | 3 +- 21 files changed, 350 insertions(+), 34 deletions(-) create mode 100644 app/components/champs/treeable.rb create mode 100644 app/components/editable_champ/champs_subtree_component.rb create mode 100644 app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml create mode 100644 app/components/editable_champ/champs_tree_component.rb create mode 100644 app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml create mode 100644 app/components/editable_champ/fields_for_champ_component.rb create mode 100644 app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml create mode 100644 app/components/editable_champ/header_section_component/header_section_component.html.haml create mode 100644 spec/components/editable_champ/champs_tree_component_spec.rb diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 1ee32deac..d2d9d0e74 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -438,14 +438,6 @@ } } - .header-section { - display: inline-block; - color: $blue-france-500; - font-size: 30px; - margin-bottom: 3 * $default-padding; - border-bottom: 3px solid $blue-france-500; - } - .header-subsection { font-size: 22px; color: $blue-france-500; diff --git a/app/assets/stylesheets/sections.scss b/app/assets/stylesheets/sections.scss index fde56d976..5a8a02673 100644 --- a/app/assets/stylesheets/sections.scss +++ b/app/assets/stylesheets/sections.scss @@ -1,11 +1,54 @@ .counter-start-header-section { - counter-reset: headerSectionCounter; -} + counter-reset: h1 h2 h3 h4 h5 h6; -.header-section { - counter-increment: headerSectionCounter; + .reset-h1 { + counter-reset: h2; + } - &.header-section-counter::before { - content: counter(headerSectionCounter) ". "; + .reset-h2 { + counter-reset: h3; + } + + .reset-h3 { + counter-reset: h4; + } + + .reset-h4 { + counter-reset: h5; + } + + .reset-h5 { + counter-reset: h6; + } + + + .header-section.fr-h1::before { + counter-increment: h1; + content: counter(h1) ". "; + } + + .header-section.fr-h2::before { + counter-increment: h2; + content: counter(h1) "."counter(h2) ". "; + } + + .header-section.fr-h3::before { + counter-increment: h3; + content: counter(h1) "."counter(h2) "." counter(h3) ". "; + } + + .header-section.fr-h4::before { + counter-increment: h4; + content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) ". "; + } + + .header-section.fr-h5::before { + counter-increment: h5; + content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". "; + } + + .header-section.fr-h6::before { + counter-increment: h6; + content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". "; } } diff --git a/app/components/champs/treeable.rb b/app/components/champs/treeable.rb new file mode 100644 index 000000000..dc47e7c6d --- /dev/null +++ b/app/components/champs/treeable.rb @@ -0,0 +1,42 @@ +module Champs::Treeable + extend ActiveSupport::Concern + + MAX_DEPTH = 6 # deepest level for header_sections is 3. + # but a repetition can be nested an header_section, so 3+3=6=MAX_DEPTH + + included do + # as we progress in the list of ordered champs + # we keep a reference to each level of nesting (depth_cache) + # when we encounter an header_section, it depends of its own depth of nesting minus 1, ie: + # h1 belongs to prior (root) + # h2 belongs to prior h1 + # h3 belongs to prior h2 + # h1 belongs to prior (root) + # then, each and every champs which are not an header_section + # are added to the most_recent_subtree + # given a root_depth at 0, we build a full tree + # given a root_depth > 0, we build a partial tree (aka, a repetition) + def to_tree(champs:, root_depth:, build_champs_subtree_component:) + root = build_champs_subtree_component(header_section: nil) + depth_cache = Array.new(MAX_DEPTH) + depth_cache[root_depth] = root + most_recent_subtree = root + + champs.each do |champ| + if champ.header_section? + champs_subtree = build_champs_subtree_component(header_section: champ) + depth_cache[champs_subtree.level - 1].add_node(champs_subtree) + most_recent_subtree = depth_cache[champs_subtree.level] = champs_subtree + else + most_recent_subtree.add_node(champ) + end + end + root + end + + # must be implemented to render subtree + def build_champs_subtree_component(header_section:) + raise NotImplementedError + end + end +end diff --git a/app/components/editable_champ/champs_subtree_component.rb b/app/components/editable_champ/champs_subtree_component.rb new file mode 100644 index 000000000..46fce32ba --- /dev/null +++ b/app/components/editable_champ/champs_subtree_component.rb @@ -0,0 +1,41 @@ +class EditableChamp::ChampsSubtreeComponent < ApplicationComponent + include ApplicationHelper + + attr_reader :header_section, :nodes + + def initialize(header_section:) + @header_section = header_section + @nodes = [] + end + + # a nodes can be either a champs, or a subtree + def add_node(node) + nodes.push(node) + end + + def render_within_fieldset? + header_section && !empty_section? + end + + def render_header_section_only? + header_section && empty_section? + end + + def empty_section? + nodes.none? { |node| node.is_a?(Champ) } + end + + def level + if header_section.parent.present? + header_section.header_section_level_value.to_i + header_section.parent.current_section_level + elsif header_section + header_section.header_section_level_value.to_i + else + 0 + end + end + + def tag_for_depth + "h#{level + 1}" + end +end diff --git a/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml b/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml new file mode 100644 index 000000000..3cd8a90ec --- /dev/null +++ b/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml @@ -0,0 +1,17 @@ +- if render_within_fieldset? + = tag.fieldset(class: "reset-#{tag_for_depth}") do + = tag.legend do + = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil, level: level) + - @nodes.each do |champ_or_section| + - if champ_or_section.is_a?(Champ) + = render EditableChamp::FieldsForChampComponent.new(champ: champ_or_section, seen_at: nil) + - else + = render champ_or_section +- elsif render_header_section_only? + = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil, level: level) +- else + - @nodes.each do |champ_or_section| + - if champ_or_section.is_a?(Champ) + = render EditableChamp::FieldsForChampComponent.new(champ: champ_or_section, seen_at: nil) + - else + = render champ_or_section diff --git a/app/components/editable_champ/champs_tree_component.rb b/app/components/editable_champ/champs_tree_component.rb new file mode 100644 index 000000000..aeb559798 --- /dev/null +++ b/app/components/editable_champ/champs_tree_component.rb @@ -0,0 +1,13 @@ +class EditableChamp::ChampsTreeComponent < ApplicationComponent + include Champs::Treeable + + attr_reader :root + + def initialize(champs:, root_depth:) + @root = to_tree(champs:, root_depth:, build_champs_subtree_component: method(:build_champs_subtree_component)) + end + + def build_champs_subtree_component(header_section:) + EditableChamp::ChampsSubtreeComponent.new(header_section:) + end +end diff --git a/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml b/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml new file mode 100644 index 000000000..1cc333e98 --- /dev/null +++ b/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml @@ -0,0 +1 @@ += render @root diff --git a/app/components/editable_champ/fields_for_champ_component.rb b/app/components/editable_champ/fields_for_champ_component.rb new file mode 100644 index 000000000..86cf1aea3 --- /dev/null +++ b/app/components/editable_champ/fields_for_champ_component.rb @@ -0,0 +1,5 @@ +class EditableChamp::FieldsForChampComponent < ApplicationComponent + def initialize(champ:, seen_at: nil) + @champ, @seen_at = champ, seen_at + end +end diff --git a/app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml b/app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml new file mode 100644 index 000000000..97b59eff8 --- /dev/null +++ b/app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml @@ -0,0 +1,2 @@ += fields_for @champ.input_name, @champ do |form| + = render EditableChamp::EditableChampComponent.new form: form, champ: @champ, seen_at: @seen_at diff --git a/app/components/editable_champ/header_section_component.rb b/app/components/editable_champ/header_section_component.rb index f5e71cfce..e3d740905 100644 --- a/app/components/editable_champ/header_section_component.rb +++ b/app/components/editable_champ/header_section_component.rb @@ -1,2 +1,22 @@ -class EditableChamp::HeaderSectionComponent < EditableChamp::EditableChampBaseComponent +class EditableChamp::HeaderSectionComponent < ApplicationComponent + def initialize(form:, champ:, seen_at: nil, level: 1) + @champ = champ + @form = form + @level = level + end + + def libelle + @champ.libelle + end + + def header_section_classnames + class_names = ["fr-h#{@level}", 'header-section'] + + class_names << 'header-section-counter' if @champ.dossier.auto_numbering_section_headers_for?(@champ) + class_names + end + + def tag_for_depth + "h#{@level + 1}" + end end diff --git a/app/components/editable_champ/header_section_component/header_section_component.html.haml b/app/components/editable_champ/header_section_component/header_section_component.html.haml new file mode 100644 index 000000000..9f3efef55 --- /dev/null +++ b/app/components/editable_champ/header_section_component/header_section_component.html.haml @@ -0,0 +1,2 @@ += tag.send(tag_for_depth, class: header_section_classnames) do + = libelle diff --git a/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml b/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml index c83cdadaa..f97957219 100644 --- a/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml +++ b/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml @@ -1,8 +1,6 @@ - row_id = "safe-row-selector-#{@row.first.row_id}" .row{ id: row_id } - - @row.each do |champ| - = fields_for champ.input_name, champ do |form| - = render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: @seen_at + = render EditableChamp::ChampsTreeComponent.new(champs: @row, root_depth: @champ.current_section_level) .flex.row-reverse{ 'data-turbo': 'true' } = render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id, row_id: @row.first.row_id), http_method: :delete, opt: { class: "fr-btn fr-btn--sm fr-btn--tertiary fr-text-action-high--red-marianne", title: t(".delete_title", row_number: @champ.rows.find_index(@row))}) do diff --git a/app/components/types_de_champ_editor/header_section_component.rb b/app/components/types_de_champ_editor/header_section_component.rb index 51fbac301..80271b8e1 100644 --- a/app/components/types_de_champ_editor/header_section_component.rb +++ b/app/components/types_de_champ_editor/header_section_component.rb @@ -9,7 +9,7 @@ class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent def header_section_options_for_select closest_level = @tdc.previous_section_level(@upper_tdcs) - next_level = [MAX_LEVEL, closest_level + 1].min + next_level = [closest_level + 1, MAX_LEVEL].min available_levels = (1..next_level).map(&method(:option_for_level)) disabled_levels = errors? ? (next_level + 1..MAX_LEVEL).map(&method(:option_for_level)) : [] @@ -21,7 +21,7 @@ class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent end def errors - @tdc.check_nesting(@upper_tdcs) + @tdc.check_coherent_header_level(@upper_tdcs) end private diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 2d492f836..588c8c2df 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -83,7 +83,12 @@ class RootController < ApplicationController end end - @dossier.association(:revision).target = @dossier.procedure.build_draft_revision + draft_revision = @dossier.procedure.build_draft_revision(types_de_champ_public: all_champs.map(&:type_de_champ)) + @dossier.association(:revision).target = draft_revision + @dossier.champs_public.map(&:type_de_champ).map do |tdc| + tdc.association(:revision_type_de_champ).target = tdc.build_revision_type_de_champ(revision: draft_revision) + tdc.association(:revision).target = draft_revision + end end def suivi diff --git a/app/models/champ.rb b/app/models/champ.rb index 53519d25d..5060baae8 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -48,7 +48,7 @@ class Champ < ApplicationRecord :drop_down_secondary_description, :collapsible_explanation_enabled?, :collapsible_explanation_text, - :header_section_level, + :header_section_level_value, :current_section_level, :exclude_from_export?, :exclude_from_view?, diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index 0ab4eabf8..5e192e166 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -418,7 +418,7 @@ class ProcedureRevision < ApplicationRecord tdcs .map.with_index .filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil } - .map { |tdc, i| [tdc, tdc.check_nesting(tdcs.take(i))] } + .map { |tdc, i| [tdc, tdc.check_coherent_header_level(tdcs.take(i))] } .filter { |_tdc, errors| errors.present? } .each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) } end diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 0e8b4c19e..0d36dece3 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -423,7 +423,7 @@ class TypeDeChamp < ApplicationRecord previous_header_section.header_section_level_value.to_i end - def check_nesting(upper_tdcs) + def check_coherent_header_level(upper_tdcs) errs = [] previous_level = previous_section_level(upper_tdcs) diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index 0ef0da88d..e3a4563bc 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -42,9 +42,5 @@ = f.select :groupe_instructeur_id, dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] }, { include_blank: dossier.brouillon? } - - - dossier.champs_public.each do |champ| - = fields_for champ.input_name, champ do |form| - = render EditableChamp::EditableChampComponent.new form: form, champ: champ - + = render EditableChamp::ChampsTreeComponent.new(champs: dossier.champs_public, root_depth: 0) = render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: false) diff --git a/app/views/shared/dossiers/_edit_annotations.html.haml b/app/views/shared/dossiers/_edit_annotations.html.haml index 295422c27..7f69a1390 100644 --- a/app/views/shared/dossiers/_edit_annotations.html.haml +++ b/app/views/shared/dossiers/_edit_annotations.html.haml @@ -3,9 +3,7 @@ %section.counter-start-header-section = render NestedForms::FormOwnerComponent.new = form_for dossier, url: annotations_instructeur_dossier_path(dossier.procedure, dossier), html: { class: 'form', multipart: true } do |f| - - dossier.champs_private.each do |champ| - = fields_for champ.input_name, champ do |form| - = render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: seen_at + = render EditableChamp::ChampsTreeComponent.new(champs: dossier.champs_private, root_depth: 0) = render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: true) - else diff --git a/spec/components/editable_champ/champs_tree_component_spec.rb b/spec/components/editable_champ/champs_tree_component_spec.rb new file mode 100644 index 000000000..6342f58e0 --- /dev/null +++ b/spec/components/editable_champ/champs_tree_component_spec.rb @@ -0,0 +1,142 @@ +describe EditableChamp::ChampsTreeComponent, type: :component do + let(:component) { described_class.new(champs: champs, root_depth: 0) } + subject { component.root } + describe "to_tree" do + let(:header_1) { build(:champ_header_section_level_1) } + let(:header_1_2) { build(:champ_header_section_level_2) } + let(:header_2) { build(:champ_header_section_level_1) } + let(:champ_text) { build(:champ_text) } + let(:champ_textarea) { build(:champ_textarea) } + let(:champ_explication) { build(:champ_explication) } + let(:champ_communes) { build(:champ_communes) } + + context 'without section' do + let(:champs) do + [ + champ_text, champ_textarea + ] + end + it 'chunk by uniq champs' do + expect(subject.header_section).to eq(nil) + expect(subject.nodes.size).to eq(champs.size) + expect(subject.nodes).to eq(champs) + end + end + + context 'with sections only' do + let(:champs) do + [ + header_1, + champ_explication, + champ_text, + header_2, + champ_textarea + ] + end + + it 'chunk by uniq champs' do + expect(subject.nodes.size).to eq(2) + expect(subject.nodes[0].header_section).to eq(header_1) + expect(subject.nodes[0].nodes).to eq([champ_explication, champ_text]) + expect(subject.nodes[1].header_section).to eq(header_2) + expect(subject.nodes[1].nodes).to eq([champ_textarea]) + end + end + + context 'leading champs, and in between sections only' do + let(:champs) do + [ + champ_text, + champ_textarea, + header_1, + champ_explication, + champ_communes, + header_2, + champ_textarea + ] + end + it 'chunk by uniq champs' do + expect(subject.nodes.size).to eq(4) + expect(subject.nodes[0]).to eq(champ_text) + expect(subject.nodes[1]).to eq(champ_textarea) + expect(subject.nodes[2].header_section).to eq(header_1) + expect(subject.nodes[2].nodes).to eq([champ_explication, champ_communes]) + expect(subject.nodes[3].header_section).to eq(header_2) + expect(subject.nodes[3].nodes).to eq([champ_textarea]) + end + end + + context 'with one sub sections' do + let(:champs) do + [ + header_1, + champ_explication, + header_1_2, + champ_communes, + header_2, + champ_textarea + ] + end + it 'chunk by uniq champs' do + expect(subject.nodes.size).to eq(2) + expect(subject.nodes[0].header_section).to eq(header_1) + expect(subject.nodes[0].nodes.size).to eq(2) + expect(subject.nodes[0].nodes[1].header_section).to eq(header_1_2) + expect(subject.nodes[0].nodes[1].nodes).to eq([champ_communes]) + expect(subject.nodes[1].header_section).to eq(header_2) + expect(subject.nodes[1].nodes).to eq([champ_textarea]) + end + end + + context 'with consecutive subsection' do + let(:header_1) { build(:champ_header_section_level_1) } + let(:header_1_2_1) { build(:champ_header_section_level_2) } + let(:header_1_2_2) { build(:champ_header_section_level_2) } + let(:header_1_2_3) { build(:champ_header_section_level_2) } + let(:champs) do + [ + header_1, + header_1_2_1, + build(:champ_text), + header_1_2_2, + build(:champ_text), + header_1_2_3, + build(:champ_text) + ] + end + it 'chunk by uniq champs' do + expect(subject.nodes.size).to eq(1) + end + end + + context 'with one sub sections and one subsub section' do + let(:header_1_2_3) { build(:champ_header_section_level_3) } + + let(:champs) do + [ + header_1, + champ_explication, + header_1_2, + champ_communes, + header_1_2_3, + champ_text, + header_2, + champ_textarea + ] + end + + it 'chunk by uniq champs' do + expect(subject.nodes.size).to eq(2) + expect(subject.nodes[0].header_section).to eq(header_1) + expect(subject.nodes[0].nodes.size).to eq(2) + expect(subject.nodes[0].nodes[1].header_section).to eq(header_1_2) + expect(subject.nodes[0].nodes[1].nodes.size).to eq(2) + expect(subject.nodes[0].nodes[1].nodes.first).to eq(champ_communes) + expect(subject.nodes[0].nodes[1].nodes[1].header_section).to eq(header_1_2_3) + expect(subject.nodes[0].nodes[1].nodes[1].nodes).to eq([champ_text]) + expect(subject.nodes[1].header_section).to eq(header_2) + expect(subject.nodes[1].nodes).to eq([champ_textarea]) + end + end + end +end diff --git a/spec/system/administrateurs/types_de_champ_spec.rb b/spec/system/administrateurs/types_de_champ_spec.rb index 1ab7e42c8..ba5ef3578 100644 --- a/spec/system/administrateurs/types_de_champ_spec.rb +++ b/spec/system/administrateurs/types_de_champ_spec.rb @@ -198,9 +198,8 @@ describe 'As an administrateur I can edit types de champ', js: true do context 'header section' do scenario 'invalid order, it pops up errors summary' do add_champ - wait_until { procedure.active_revision.types_de_champ_public.size == 1 } - first_header = procedure.active_revision.types_de_champ_public.first select('Titre de section', from: 'Type de champ') + first_header = procedure.active_revision.types_de_champ_public.first select('Titre de niveau 1', from: dom_id(first_header, :header_section_level)) add_champ From 3b5d1bd55b48f36a6c607ca69b1b5752b9d7cbc1 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 11 Apr 2023 13:53:59 +0200 Subject: [PATCH 52/75] tech(remaniement): extraction du passage d'une liste de champs a un simili arbre --- app/models/champs/header_section_champ.rb | 11 ++ .../concerns/treeable_concern.rb} | 19 +- app/models/type_de_champ.rb | 7 +- spec/models/concern/treeable_concern_spec.rb | 162 ++++++++++++++++++ 4 files changed, 186 insertions(+), 13 deletions(-) rename app/{components/champs/treeable.rb => models/concerns/treeable_concern.rb} (63%) create mode 100644 spec/models/concern/treeable_concern_spec.rb diff --git a/app/models/champs/header_section_champ.rb b/app/models/champs/header_section_champ.rb index 4185db524..59e4b5755 100644 --- a/app/models/champs/header_section_champ.rb +++ b/app/models/champs/header_section_champ.rb @@ -21,6 +21,17 @@ # type_de_champ_id :integer # class Champs::HeaderSectionChamp < Champ + + def level + if parent.present? + header_section_level_value.to_i + parent.current_section_level + elsif header_section_level_value + header_section_level_value.to_i + else + 0 + end + end + def search_terms # The user cannot enter any information here so it doesn’t make much sense to search end diff --git a/app/components/champs/treeable.rb b/app/models/concerns/treeable_concern.rb similarity index 63% rename from app/components/champs/treeable.rb rename to app/models/concerns/treeable_concern.rb index dc47e7c6d..3f269e11e 100644 --- a/app/components/champs/treeable.rb +++ b/app/models/concerns/treeable_concern.rb @@ -1,4 +1,4 @@ -module Champs::Treeable +module TreeableConcern extend ActiveSupport::Concern MAX_DEPTH = 6 # deepest level for header_sections is 3. @@ -16,27 +16,22 @@ module Champs::Treeable # are added to the most_recent_subtree # given a root_depth at 0, we build a full tree # given a root_depth > 0, we build a partial tree (aka, a repetition) - def to_tree(champs:, root_depth:, build_champs_subtree_component:) - root = build_champs_subtree_component(header_section: nil) + def to_tree(champs:, root_depth:) + root = [] depth_cache = Array.new(MAX_DEPTH) depth_cache[root_depth] = root most_recent_subtree = root champs.each do |champ| if champ.header_section? - champs_subtree = build_champs_subtree_component(header_section: champ) - depth_cache[champs_subtree.level - 1].add_node(champs_subtree) - most_recent_subtree = depth_cache[champs_subtree.level] = champs_subtree + champs_subtree = [champ] + depth_cache[champ.level - 1].push(champs_subtree) + most_recent_subtree = depth_cache[champ.level] = champs_subtree else - most_recent_subtree.add_node(champ) + most_recent_subtree.push(champ) end end root end - - # must be implemented to render subtree - def build_champs_subtree_component(header_section:) - raise NotImplementedError - end end end diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 0d36dece3..e7c521de5 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -413,9 +413,14 @@ class TypeDeChamp < ApplicationRecord end def header_section_level_value - header_section_level.presence || 1 + if header_section_level.presence + header_section_level.to_i + else + 1 + end end + def previous_section_level(upper_tdcs) previous_header_section = upper_tdcs.reverse.find(&:header_section?) diff --git a/spec/models/concern/treeable_concern_spec.rb b/spec/models/concern/treeable_concern_spec.rb new file mode 100644 index 000000000..c2757a3a2 --- /dev/null +++ b/spec/models/concern/treeable_concern_spec.rb @@ -0,0 +1,162 @@ +describe TreeableConcern do + class ChampsToTree + include TreeableConcern + attr_reader :root + def initialize(champs:, root_depth:) + @root = to_tree(champs:, root_depth:) + end + end + + subject { ChampsToTree.new(champs: champs, root_depth: 0).root } + describe "to_tree" do + let(:header_1) { build(:champ_header_section_level_1) } + let(:header_1_2) { build(:champ_header_section_level_2) } + let(:header_2) { build(:champ_header_section_level_1) } + let(:champ_text) { build(:champ_text) } + let(:champ_textarea) { build(:champ_textarea) } + let(:champ_explication) { build(:champ_explication) } + let(:champ_communes) { build(:champ_communes) } + + context 'without section' do + let(:champs) do + [ + champ_text, champ_textarea + ] + end + it 'inlines champs at root level' do + expect(subject.size).to eq(champs.size) + expect(subject).to eq(champs) + end + end + + context 'with header_section and champs' do + let(:champs) do + [ + header_1, + champ_explication, + champ_text, + header_2, + champ_textarea + ] + end + + it 'wraps champs within preview header section' do + expect(subject.size).to eq(2) + expect(subject).to eq([ + [ header_1, champ_explication, champ_text ], + [ header_2, champ_textarea ] + ]) + end + end + + context 'leading champs, and in between sections only' do + let(:champs) do + [ + champ_text, + champ_textarea, + header_1, + champ_explication, + champ_communes, + header_2, + champ_textarea + ] + end + it 'chunk by uniq champs' do + expect(subject.size).to eq(4) + expect(subject).to eq([ + champ_text, + champ_textarea, + [header_1, champ_explication, champ_communes], + [header_2, champ_textarea] + ]) + end + end + + context 'with one sub sections' do + let(:champs) do + [ + header_1, + champ_explication, + header_1_2, + champ_communes, + header_2, + champ_textarea + ] + end + it 'chunk by uniq champs' do + expect(subject.size).to eq(2) + expect(subject).to eq([ + [header_1, champ_explication, [header_1_2, champ_communes]], + [header_2, champ_textarea] + ]) + end + end + + context 'with consecutive subsection' do + let(:header_1) { build(:champ_header_section_level_1) } + let(:header_1_2_1) { build(:champ_header_section_level_2) } + let(:header_1_2_2) { build(:champ_header_section_level_2) } + let(:header_1_2_3) { build(:champ_header_section_level_2) } + let(:champs) do + [ + header_1, + header_1_2_1, + champ_text, + header_1_2_2, + champ_textarea, + header_1_2_3, + champ_communes + ] + end + it 'chunk by uniq champs' do + expect(subject.size).to eq(1) + expect(subject).to eq([ + [ + header_1, + [header_1_2_1, champ_text], + [header_1_2_2, champ_textarea], + [header_1_2_3, champ_communes] + ] + ]) + end + end + + context 'with one sub sections and one subsub section' do + let(:header_1_2_3) { build(:champ_header_section_level_3) } + + let(:champs) do + [ + header_1, + champ_explication, + header_1_2, + champ_communes, + header_1_2_3, + champ_text, + header_2, + champ_textarea + ] + end + + it 'chunk by uniq champs' do + expect(subject.size).to eq(2) + expect(subject).to eq([ + [ + header_1, + champ_explication, + [ + header_1_2, + champ_communes, + [ + header_1_2_3, champ_text + ] + ] + ], + [ + header_2, + champ_textarea + ] + ]) + end + end + end +end From 88abefb370f9229af40b3e14a6565699716bad0a Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 12 Apr 2023 17:55:08 +0200 Subject: [PATCH 53/75] tech(remaniement): isole la logique de rendu au champs_subtree_component --- .../champs_subtree_component.rb | 56 ++++--- .../champs_subtree_component.html.haml | 14 +- .../editable_champ/champs_tree_component.rb | 10 +- .../champs_tree_component.html.haml | 2 +- .../header_section_component.rb | 11 +- app/models/champs/header_section_champ.rb | 1 - app/models/type_de_champ.rb | 1 - .../champs_subtree_component_spec.rb | 86 +++++++++++ .../champs_tree_component_spec.rb | 142 ------------------ spec/models/concern/treeable_concern_spec.rb | 5 +- 10 files changed, 137 insertions(+), 191 deletions(-) create mode 100644 spec/components/editable_champ/champs_subtree_component_spec.rb delete mode 100644 spec/components/editable_champ/champs_tree_component_spec.rb diff --git a/app/components/editable_champ/champs_subtree_component.rb b/app/components/editable_champ/champs_subtree_component.rb index 46fce32ba..028aeee81 100644 --- a/app/components/editable_champ/champs_subtree_component.rb +++ b/app/components/editable_champ/champs_subtree_component.rb @@ -1,41 +1,47 @@ class EditableChamp::ChampsSubtreeComponent < ApplicationComponent include ApplicationHelper + include TreeableConcern - attr_reader :header_section, :nodes - - def initialize(header_section:) - @header_section = header_section - @nodes = [] - end - - # a nodes can be either a champs, or a subtree - def add_node(node) - nodes.push(node) + def initialize(nodes:) + @nodes = to_fieldset(nodes:) end def render_within_fieldset? - header_section && !empty_section? + first_champ_is_an_header_section? && any_champ_fillable? end - def render_header_section_only? - header_section && empty_section? + def header_section + first_champ = @nodes.first + return first_champ if first_champ.is_a?(Champs::HeaderSectionChamp) + nil end - def empty_section? - nodes.none? { |node| node.is_a?(Champ) } - end + def champs + return @nodes if !first_champ_is_an_header_section? + _, *rest_of_champ = @nodes - def level - if header_section.parent.present? - header_section.header_section_level_value.to_i + header_section.parent.current_section_level - elsif header_section - header_section.header_section_level_value.to_i - else - 0 - end + rest_of_champ end def tag_for_depth - "h#{level + 1}" + "h#{header_section.level + 1}" + end + + def fillable? + false + end + + private + + def to_fieldset(nodes:) + nodes.map { _1.is_a?(Array) ? EditableChamp::ChampsSubtreeComponent.new(nodes: _1) : _1 } + end + + def first_champ_is_an_header_section? + header_section.present? + end + + def any_champ_fillable? + champs.any? { _1&.fillable? } end end diff --git a/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml b/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml index 3cd8a90ec..d935cc765 100644 --- a/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml +++ b/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml @@ -1,17 +1,17 @@ - if render_within_fieldset? = tag.fieldset(class: "reset-#{tag_for_depth}") do = tag.legend do - = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil, level: level) - - @nodes.each do |champ_or_section| - - if champ_or_section.is_a?(Champ) + = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil) + - champs.each do |champ_or_section| + - if !champ_or_section.is_a?(EditableChamp::ChampsSubtreeComponent) = render EditableChamp::FieldsForChampComponent.new(champ: champ_or_section, seen_at: nil) - else = render champ_or_section -- elsif render_header_section_only? - = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil, level: level) - else - - @nodes.each do |champ_or_section| - - if champ_or_section.is_a?(Champ) + - if header_section + = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil) + - champs.each do |champ_or_section| + - if !champ_or_section.is_a?(EditableChamp::ChampsSubtreeComponent) = render EditableChamp::FieldsForChampComponent.new(champ: champ_or_section, seen_at: nil) - else = render champ_or_section diff --git a/app/components/editable_champ/champs_tree_component.rb b/app/components/editable_champ/champs_tree_component.rb index aeb559798..f9677006e 100644 --- a/app/components/editable_champ/champs_tree_component.rb +++ b/app/components/editable_champ/champs_tree_component.rb @@ -1,13 +1,7 @@ class EditableChamp::ChampsTreeComponent < ApplicationComponent - include Champs::Treeable - - attr_reader :root + include TreeableConcern def initialize(champs:, root_depth:) - @root = to_tree(champs:, root_depth:, build_champs_subtree_component: method(:build_champs_subtree_component)) - end - - def build_champs_subtree_component(header_section:) - EditableChamp::ChampsSubtreeComponent.new(header_section:) + @tree = to_tree(champs:, root_depth:) end end diff --git a/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml b/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml index 1cc333e98..6e1f1d58b 100644 --- a/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml +++ b/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml @@ -1 +1 @@ -= render @root += render EditableChamp::ChampsSubtreeComponent.new(nodes: @tree) diff --git a/app/components/editable_champ/header_section_component.rb b/app/components/editable_champ/header_section_component.rb index e3d740905..212320bc5 100644 --- a/app/components/editable_champ/header_section_component.rb +++ b/app/components/editable_champ/header_section_component.rb @@ -1,8 +1,11 @@ class EditableChamp::HeaderSectionComponent < ApplicationComponent - def initialize(form:, champ:, seen_at: nil, level: 1) + def initialize(form:, champ:, seen_at: nil) @champ = champ @form = form - @level = level + end + + def level + @champ.level end def libelle @@ -10,13 +13,13 @@ class EditableChamp::HeaderSectionComponent < ApplicationComponent end def header_section_classnames - class_names = ["fr-h#{@level}", 'header-section'] + class_names = ["fr-h#{level}", 'header-section'] class_names << 'header-section-counter' if @champ.dossier.auto_numbering_section_headers_for?(@champ) class_names end def tag_for_depth - "h#{@level + 1}" + "h#{level + 1}" end end diff --git a/app/models/champs/header_section_champ.rb b/app/models/champs/header_section_champ.rb index 59e4b5755..864179e6f 100644 --- a/app/models/champs/header_section_champ.rb +++ b/app/models/champs/header_section_champ.rb @@ -21,7 +21,6 @@ # type_de_champ_id :integer # class Champs::HeaderSectionChamp < Champ - def level if parent.present? header_section_level_value.to_i + parent.current_section_level diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index e7c521de5..c500472ce 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -420,7 +420,6 @@ class TypeDeChamp < ApplicationRecord end end - def previous_section_level(upper_tdcs) previous_header_section = upper_tdcs.reverse.find(&:header_section?) diff --git a/spec/components/editable_champ/champs_subtree_component_spec.rb b/spec/components/editable_champ/champs_subtree_component_spec.rb new file mode 100644 index 000000000..792aa7140 --- /dev/null +++ b/spec/components/editable_champ/champs_subtree_component_spec.rb @@ -0,0 +1,86 @@ +describe EditableChamp::ChampsSubtreeComponent, type: :component do + include TreeableConcern + let(:component) { described_class.new(nodes: nodes) } + let(:nodes) { to_tree(champs:, root_depth:) } + let(:root_depth) { 0 } + before { render_inline(component).to_html } + + context 'list of champs without an header_section' do + let(:champs) { [build(:champ_text), build(:champ_textarea)] } + + it 'does not render fieldset' do + expect(page).not_to have_selector("fieldset") + end + + it 'renders champs' do + expect(page).to have_selector("input[type=text]", count: 1) + expect(page).to have_selector("textarea", count: 1) + end + end + + context 'list of champs with an header_section' do + let(:champs) { [build(:champ_header_section_level_1), build(:champ_text), build(:champ_textarea)] } + + it 'renders fieldset' do + expect(page).to have_selector("fieldset") + expect(page).to have_selector("legend h2") + end + + it 'renders champs within fieldset' do + expect(page).to have_selector("fieldset input[type=text]") + expect(page).to have_selector("fieldset textarea") + end + end + + context 'list of champs without section and an header_section having champs' do + let(:champs) { [build(:champ_text), build(:champ_header_section_level_1), build(:champ_text)] } + + it 'renders fieldset' do + expect(page).to have_selector("fieldset") + expect(page).to have_selector("legend h2") + end + + it 'renders all champs, one outside fieldset, one within fieldset' do + expect(page).to have_selector("input[type=text]", count: 2) + expect(page).to have_selector("fieldset input[type=text]", count: 1) + end + end + + context 'list of header_section without champs' do + let(:champs) { [build(:champ_header_section_level_1), build(:champ_header_section_level_2), build(:champ_header_section_level_3)] } + + it 'does not render header within fieldset' do + expect(page).not_to have_selector("fieldset") + expect(page).to have_selector("h2") + expect(page).to have_selector("h3") + expect(page).to have_selector("h4") + end + end + + context 'header_section followed by explication and another fieldset' do + let(:champs) { [build(:champ_header_section_level_1), build(:champ_explication), build(:champ_header_section_level_1), build(:champ_text)] } + + it 'render fieldset, header_section (one within fieldset, one outside), also render explication' do + expect(page).to have_selector("h2", count: 2) + expect(page).to have_selector("h3") # explication + expect(page).to have_selector("fieldset h2", count: 1) + expect(page).to have_selector("fieldset input[type=text]", count: 1) + end + end + + context 'nested fieldsset' do + let(:champs) { [build(:champ_header_section_level_1), build(:champ_text), build(:champ_header_section_level_2), build(:champ_textarea)] } + + it 'render nested fieldsets' do + expect(page).to have_selector("fieldset") + expect(page).to have_selector("legend h2") + expect(page).to have_selector("fieldset fieldset") + expect(page).to have_selector("fieldset fieldset legend h3") + end + + it 'contains all champs' do + expect(page).to have_selector("fieldset input[type=text]", count: 1) + expect(page).to have_selector("fieldset fieldset textarea", count: 1) + end + end +end diff --git a/spec/components/editable_champ/champs_tree_component_spec.rb b/spec/components/editable_champ/champs_tree_component_spec.rb deleted file mode 100644 index 6342f58e0..000000000 --- a/spec/components/editable_champ/champs_tree_component_spec.rb +++ /dev/null @@ -1,142 +0,0 @@ -describe EditableChamp::ChampsTreeComponent, type: :component do - let(:component) { described_class.new(champs: champs, root_depth: 0) } - subject { component.root } - describe "to_tree" do - let(:header_1) { build(:champ_header_section_level_1) } - let(:header_1_2) { build(:champ_header_section_level_2) } - let(:header_2) { build(:champ_header_section_level_1) } - let(:champ_text) { build(:champ_text) } - let(:champ_textarea) { build(:champ_textarea) } - let(:champ_explication) { build(:champ_explication) } - let(:champ_communes) { build(:champ_communes) } - - context 'without section' do - let(:champs) do - [ - champ_text, champ_textarea - ] - end - it 'chunk by uniq champs' do - expect(subject.header_section).to eq(nil) - expect(subject.nodes.size).to eq(champs.size) - expect(subject.nodes).to eq(champs) - end - end - - context 'with sections only' do - let(:champs) do - [ - header_1, - champ_explication, - champ_text, - header_2, - champ_textarea - ] - end - - it 'chunk by uniq champs' do - expect(subject.nodes.size).to eq(2) - expect(subject.nodes[0].header_section).to eq(header_1) - expect(subject.nodes[0].nodes).to eq([champ_explication, champ_text]) - expect(subject.nodes[1].header_section).to eq(header_2) - expect(subject.nodes[1].nodes).to eq([champ_textarea]) - end - end - - context 'leading champs, and in between sections only' do - let(:champs) do - [ - champ_text, - champ_textarea, - header_1, - champ_explication, - champ_communes, - header_2, - champ_textarea - ] - end - it 'chunk by uniq champs' do - expect(subject.nodes.size).to eq(4) - expect(subject.nodes[0]).to eq(champ_text) - expect(subject.nodes[1]).to eq(champ_textarea) - expect(subject.nodes[2].header_section).to eq(header_1) - expect(subject.nodes[2].nodes).to eq([champ_explication, champ_communes]) - expect(subject.nodes[3].header_section).to eq(header_2) - expect(subject.nodes[3].nodes).to eq([champ_textarea]) - end - end - - context 'with one sub sections' do - let(:champs) do - [ - header_1, - champ_explication, - header_1_2, - champ_communes, - header_2, - champ_textarea - ] - end - it 'chunk by uniq champs' do - expect(subject.nodes.size).to eq(2) - expect(subject.nodes[0].header_section).to eq(header_1) - expect(subject.nodes[0].nodes.size).to eq(2) - expect(subject.nodes[0].nodes[1].header_section).to eq(header_1_2) - expect(subject.nodes[0].nodes[1].nodes).to eq([champ_communes]) - expect(subject.nodes[1].header_section).to eq(header_2) - expect(subject.nodes[1].nodes).to eq([champ_textarea]) - end - end - - context 'with consecutive subsection' do - let(:header_1) { build(:champ_header_section_level_1) } - let(:header_1_2_1) { build(:champ_header_section_level_2) } - let(:header_1_2_2) { build(:champ_header_section_level_2) } - let(:header_1_2_3) { build(:champ_header_section_level_2) } - let(:champs) do - [ - header_1, - header_1_2_1, - build(:champ_text), - header_1_2_2, - build(:champ_text), - header_1_2_3, - build(:champ_text) - ] - end - it 'chunk by uniq champs' do - expect(subject.nodes.size).to eq(1) - end - end - - context 'with one sub sections and one subsub section' do - let(:header_1_2_3) { build(:champ_header_section_level_3) } - - let(:champs) do - [ - header_1, - champ_explication, - header_1_2, - champ_communes, - header_1_2_3, - champ_text, - header_2, - champ_textarea - ] - end - - it 'chunk by uniq champs' do - expect(subject.nodes.size).to eq(2) - expect(subject.nodes[0].header_section).to eq(header_1) - expect(subject.nodes[0].nodes.size).to eq(2) - expect(subject.nodes[0].nodes[1].header_section).to eq(header_1_2) - expect(subject.nodes[0].nodes[1].nodes.size).to eq(2) - expect(subject.nodes[0].nodes[1].nodes.first).to eq(champ_communes) - expect(subject.nodes[0].nodes[1].nodes[1].header_section).to eq(header_1_2_3) - expect(subject.nodes[0].nodes[1].nodes[1].nodes).to eq([champ_text]) - expect(subject.nodes[1].header_section).to eq(header_2) - expect(subject.nodes[1].nodes).to eq([champ_textarea]) - end - end - end -end diff --git a/spec/models/concern/treeable_concern_spec.rb b/spec/models/concern/treeable_concern_spec.rb index c2757a3a2..9887df4f3 100644 --- a/spec/models/concern/treeable_concern_spec.rb +++ b/spec/models/concern/treeable_concern_spec.rb @@ -1,6 +1,7 @@ describe TreeableConcern do class ChampsToTree include TreeableConcern + attr_reader :root def initialize(champs:, root_depth:) @root = to_tree(champs:, root_depth:) @@ -43,8 +44,8 @@ describe TreeableConcern do it 'wraps champs within preview header section' do expect(subject.size).to eq(2) expect(subject).to eq([ - [ header_1, champ_explication, champ_text ], - [ header_2, champ_textarea ] + [header_1, champ_explication, champ_text], + [header_2, champ_textarea] ]) end end From cc2c856ec2a6abf97acc0f0e96977d4d570fb293 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 13 Apr 2023 15:09:24 +0200 Subject: [PATCH 54/75] amelioration(bloc-repetable): wrap les blocs repetatable dans des fieldsets --- app/assets/stylesheets/sections.scss | 9 +++++++++ .../editable_champ/editable_champ_component.rb | 6 +++++- .../editable_champ_component.html.haml | 7 +------ .../editable_champ/repetition_component.rb | 7 +++++++ .../repetition_component.html.haml | 18 ++++++++++++------ .../repetition_row_component.html.haml | 7 ++++++- .../shared_examples_for_prefilled_dossier.rb | 2 +- 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/sections.scss b/app/assets/stylesheets/sections.scss index 5a8a02673..107091ff8 100644 --- a/app/assets/stylesheets/sections.scss +++ b/app/assets/stylesheets/sections.scss @@ -51,4 +51,13 @@ counter-increment: h6; content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". "; } + + .repetition { + counter-reset: repetition; + + .block-id::after { + counter-increment: repetition; + content: counter(repetition); + } + } } diff --git a/app/components/editable_champ/editable_champ_component.rb b/app/components/editable_champ/editable_champ_component.rb index 1b1e78132..d0cde5461 100644 --- a/app/components/editable_champ/editable_champ_component.rb +++ b/app/components/editable_champ/editable_champ_component.rb @@ -6,7 +6,11 @@ class EditableChamp::EditableChampComponent < ApplicationComponent private def has_label?(champ) - types_without_label = [TypeDeChamp.type_champs.fetch(:header_section), TypeDeChamp.type_champs.fetch(:explication)] + types_without_label = [ + TypeDeChamp.type_champs.fetch(:header_section), + TypeDeChamp.type_champs.fetch(:explication), + TypeDeChamp.type_champs.fetch(:repetition) + ] !types_without_label.include?(@champ.type_champ) end diff --git a/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml b/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml index 165fa6b24..c73824c81 100644 --- a/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml +++ b/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml @@ -1,10 +1,5 @@ .editable-champ{ html_options } - - if @champ.block? - %h3.header-subsection= @champ.libelle - - if @champ.description.present? - .notice= render SimpleFormatComponent.new(@champ.description, allow_a: true) - - - elsif has_label?(@champ) + - if has_label?(@champ) = render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at - if @champ.titre_identite? %p.notice= t('.titre_identite_notice') diff --git a/app/components/editable_champ/repetition_component.rb b/app/components/editable_champ/repetition_component.rb index 03fba3de5..bd077dd9b 100644 --- a/app/components/editable_champ/repetition_component.rb +++ b/app/components/editable_champ/repetition_component.rb @@ -1,2 +1,9 @@ class EditableChamp::RepetitionComponent < EditableChamp::EditableChampBaseComponent + def legend_params + @champ.description.present? ? { describedby: dom_id(@champ, :repetition) } : {} + end + + def notice_params + @champ.description.present? ? { id: dom_id(@champ, :repetition) } : {} + end end diff --git a/app/components/editable_champ/repetition_component/repetition_component.html.haml b/app/components/editable_champ/repetition_component/repetition_component.html.haml index 1bcbe91a3..1a434b96b 100644 --- a/app/components/editable_champ/repetition_component/repetition_component.html.haml +++ b/app/components/editable_champ/repetition_component/repetition_component.html.haml @@ -1,7 +1,13 @@ -.repetition{ id: dom_id(@champ, :rows) } - - @champ.rows.each do |champs| - = render EditableChamp::RepetitionRowComponent.new(form: @form, champ: @champ, row: champs, seen_at: @seen_at) +%fieldset + %legend.header-subsection{ legend_params }= @champ.libelle + - if @champ.description.present? + .notice{ notice_params }= render SimpleFormatComponent.new(@champ.description, allow_a: true) -.actions{ 'data-turbo': 'true' } - = render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id), http_method: :create, opt: { class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-circle-line fr-mb-3w", title: t(".add_title", libelle: @champ.libelle), id: dom_id(@champ, :create_repetition)}) do - = t(".add", libelle: @champ.libelle) + + .repetition{ id: dom_id(@champ, :rows) } + - @champ.rows.each do |champs| + = render EditableChamp::RepetitionRowComponent.new(form: @form, champ: @champ, row: champs, seen_at: @seen_at) + + .actions{ 'data-turbo': 'true' } + = render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id), http_method: :create, opt: { class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-circle-line fr-mb-3w", title: t(".add_title", libelle: @champ.libelle), id: dom_id(@champ, :create_repetition)}) do + = t(".add", libelle: @champ.libelle) diff --git a/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml b/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml index f97957219..24068e02f 100644 --- a/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml +++ b/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml @@ -1,6 +1,11 @@ - row_id = "safe-row-selector-#{@row.first.row_id}" .row{ id: row_id } - = render EditableChamp::ChampsTreeComponent.new(champs: @row, root_depth: @champ.current_section_level) + - if @row.size > 1 + %fieldset + %legend.block-id= "#{@champ.libelle} " + = render EditableChamp::ChampsTreeComponent.new(champs: @row, root_depth: @champ.current_section_level) + - else + = render EditableChamp::ChampsTreeComponent.new(champs: @row, root_depth: @champ.current_section_level) .flex.row-reverse{ 'data-turbo': 'true' } = render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id, row_id: @row.first.row_id), http_method: :delete, opt: { class: "fr-btn fr-btn--sm fr-btn--tertiary fr-text-action-high--red-marianne", title: t(".delete_title", row_number: @champ.rows.find_index(@row))}) do diff --git a/spec/support/shared_examples_for_prefilled_dossier.rb b/spec/support/shared_examples_for_prefilled_dossier.rb index 2657b0a89..1b3f1607c 100644 --- a/spec/support/shared_examples_for_prefilled_dossier.rb +++ b/spec/support/shared_examples_for_prefilled_dossier.rb @@ -16,7 +16,7 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do expect(page).to have_css('label', text: type_de_champ_phone.libelle) expect(page).to have_field(type_de_champ_rna.libelle, with: rna_value) expect(page).to have_field(type_de_champ_siret.libelle, with: siret_value) - expect(page).to have_css('h3', text: type_de_champ_repetition.libelle) + expect(page).to have_css('legend', text: type_de_champ_repetition.libelle) expect(page).to have_field(text_repetition_libelle, with: text_repetition_value) expect(page).to have_field(integer_repetition_libelle, with: integer_repetition_value) expect(page).to have_field(type_de_champ_datetime.libelle, with: datetime_value) From e64ac79f05b502ba954db219121dcd38c47f976a Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 14 Apr 2023 10:55:36 +0200 Subject: [PATCH 55/75] tech(refactor): much nicer code, thx LeSim root -> rooted_tree depth_cach -> walk smal refactor comment remove form for header_section remove seen_at from header section header_section: champ -> header_section champ_subree: remove if not remove root_depth use header_section_level_value instead remove unused include remove ChampTreeComponent rename ChampsSubtreeComponent to SectionComponent use TreeableConcern only in section component remove fields_for_champ_component champs -> tail add split_section_champ helper refactor(editable_champ::header_section): keep same interface everywhere fix(repetition): add spec for SectionComponent on repetitions --- .../champs_subtree_component.rb | 47 -------------- .../champs_subtree_component.html.haml | 17 ----- .../editable_champ/champs_tree_component.rb | 7 -- .../champs_tree_component.html.haml | 1 - .../fields_for_champ_component.rb | 5 -- .../fields_for_champ_component.html.haml | 2 - .../header_section_component.rb | 7 +- .../repetition_row_component.html.haml | 4 +- .../editable_champ/section_component.rb | 64 +++++++++++++++++++ .../section_component.html.haml | 19 ++++++ .../types_de_champ_controller.rb | 2 +- app/models/concerns/treeable_concern.rb | 28 ++++---- app/views/shared/dossiers/_edit.html.haml | 2 +- .../dossiers/_edit_annotations.html.haml | 2 +- ...nent_spec.rb => section_component_spec.rb} | 35 ++++++++-- spec/models/concern/treeable_concern_spec.rb | 6 +- 16 files changed, 139 insertions(+), 109 deletions(-) delete mode 100644 app/components/editable_champ/champs_subtree_component.rb delete mode 100644 app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml delete mode 100644 app/components/editable_champ/champs_tree_component.rb delete mode 100644 app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml delete mode 100644 app/components/editable_champ/fields_for_champ_component.rb delete mode 100644 app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml create mode 100644 app/components/editable_champ/section_component.rb create mode 100644 app/components/editable_champ/section_component/section_component.html.haml rename spec/components/editable_champ/{champs_subtree_component_spec.rb => section_component_spec.rb} (72%) diff --git a/app/components/editable_champ/champs_subtree_component.rb b/app/components/editable_champ/champs_subtree_component.rb deleted file mode 100644 index 028aeee81..000000000 --- a/app/components/editable_champ/champs_subtree_component.rb +++ /dev/null @@ -1,47 +0,0 @@ -class EditableChamp::ChampsSubtreeComponent < ApplicationComponent - include ApplicationHelper - include TreeableConcern - - def initialize(nodes:) - @nodes = to_fieldset(nodes:) - end - - def render_within_fieldset? - first_champ_is_an_header_section? && any_champ_fillable? - end - - def header_section - first_champ = @nodes.first - return first_champ if first_champ.is_a?(Champs::HeaderSectionChamp) - nil - end - - def champs - return @nodes if !first_champ_is_an_header_section? - _, *rest_of_champ = @nodes - - rest_of_champ - end - - def tag_for_depth - "h#{header_section.level + 1}" - end - - def fillable? - false - end - - private - - def to_fieldset(nodes:) - nodes.map { _1.is_a?(Array) ? EditableChamp::ChampsSubtreeComponent.new(nodes: _1) : _1 } - end - - def first_champ_is_an_header_section? - header_section.present? - end - - def any_champ_fillable? - champs.any? { _1&.fillable? } - end -end diff --git a/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml b/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml deleted file mode 100644 index d935cc765..000000000 --- a/app/components/editable_champ/champs_subtree_component/champs_subtree_component.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- if render_within_fieldset? - = tag.fieldset(class: "reset-#{tag_for_depth}") do - = tag.legend do - = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil) - - champs.each do |champ_or_section| - - if !champ_or_section.is_a?(EditableChamp::ChampsSubtreeComponent) - = render EditableChamp::FieldsForChampComponent.new(champ: champ_or_section, seen_at: nil) - - else - = render champ_or_section -- else - - if header_section - = render EditableChamp::HeaderSectionComponent.new(champ: header_section, form: nil) - - champs.each do |champ_or_section| - - if !champ_or_section.is_a?(EditableChamp::ChampsSubtreeComponent) - = render EditableChamp::FieldsForChampComponent.new(champ: champ_or_section, seen_at: nil) - - else - = render champ_or_section diff --git a/app/components/editable_champ/champs_tree_component.rb b/app/components/editable_champ/champs_tree_component.rb deleted file mode 100644 index f9677006e..000000000 --- a/app/components/editable_champ/champs_tree_component.rb +++ /dev/null @@ -1,7 +0,0 @@ -class EditableChamp::ChampsTreeComponent < ApplicationComponent - include TreeableConcern - - def initialize(champs:, root_depth:) - @tree = to_tree(champs:, root_depth:) - end -end diff --git a/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml b/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml deleted file mode 100644 index 6e1f1d58b..000000000 --- a/app/components/editable_champ/champs_tree_component/champs_tree_component.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render EditableChamp::ChampsSubtreeComponent.new(nodes: @tree) diff --git a/app/components/editable_champ/fields_for_champ_component.rb b/app/components/editable_champ/fields_for_champ_component.rb deleted file mode 100644 index 86cf1aea3..000000000 --- a/app/components/editable_champ/fields_for_champ_component.rb +++ /dev/null @@ -1,5 +0,0 @@ -class EditableChamp::FieldsForChampComponent < ApplicationComponent - def initialize(champ:, seen_at: nil) - @champ, @seen_at = champ, seen_at - end -end diff --git a/app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml b/app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml deleted file mode 100644 index 97b59eff8..000000000 --- a/app/components/editable_champ/fields_for_champ_component/fields_for_champ_component.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= fields_for @champ.input_name, @champ do |form| - = render EditableChamp::EditableChampComponent.new form: form, champ: @champ, seen_at: @seen_at diff --git a/app/components/editable_champ/header_section_component.rb b/app/components/editable_champ/header_section_component.rb index 212320bc5..253e32ff0 100644 --- a/app/components/editable_champ/header_section_component.rb +++ b/app/components/editable_champ/header_section_component.rb @@ -1,7 +1,6 @@ class EditableChamp::HeaderSectionComponent < ApplicationComponent - def initialize(form:, champ:, seen_at: nil) + def initialize(form: nil, champ:, seen_at: nil) @champ = champ - @form = form end def level @@ -13,9 +12,9 @@ class EditableChamp::HeaderSectionComponent < ApplicationComponent end def header_section_classnames - class_names = ["fr-h#{level}", 'header-section'] + class_names = ["fr-h#{level}"] - class_names << 'header-section-counter' if @champ.dossier.auto_numbering_section_headers_for?(@champ) + class_names << 'header-section' if @champ.dossier.auto_numbering_section_headers_for?(@champ) class_names end diff --git a/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml b/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml index 24068e02f..addb1c152 100644 --- a/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml +++ b/app/components/editable_champ/repetition_row_component/repetition_row_component.html.haml @@ -3,9 +3,9 @@ - if @row.size > 1 %fieldset %legend.block-id= "#{@champ.libelle} " - = render EditableChamp::ChampsTreeComponent.new(champs: @row, root_depth: @champ.current_section_level) + = render EditableChamp::SectionComponent.new(champs: @row) - else - = render EditableChamp::ChampsTreeComponent.new(champs: @row, root_depth: @champ.current_section_level) + = render EditableChamp::SectionComponent.new(champs: @row) .flex.row-reverse{ 'data-turbo': 'true' } = render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id, row_id: @row.first.row_id), http_method: :delete, opt: { class: "fr-btn fr-btn--sm fr-btn--tertiary fr-text-action-high--red-marianne", title: t(".delete_title", row_number: @champ.rows.find_index(@row))}) do diff --git a/app/components/editable_champ/section_component.rb b/app/components/editable_champ/section_component.rb new file mode 100644 index 000000000..c8ad567a1 --- /dev/null +++ b/app/components/editable_champ/section_component.rb @@ -0,0 +1,64 @@ +class EditableChamp::SectionComponent < ApplicationComponent + include ApplicationHelper + include TreeableConcern + + def initialize(nodes: nil, champs: nil) + if (nodes.nil?) + nodes = to_tree(champs:) + end + @nodes = to_fieldset(nodes:) + end + + def render_within_fieldset? + first_champ_is_an_header_section? && any_champ_fillable? + end + + def header_section + return @nodes.first if @nodes.first.is_a?(Champs::HeaderSectionChamp) + end + + def splitted_tail + tail.map { split_section_champ(_1) } + end + + def tail + return @nodes if !first_champ_is_an_header_section? + _, *rest_of_champ = @nodes + + rest_of_champ + end + + def tag_for_depth + "h#{header_section.level + 1}" + end + + # if two headers follows each others [h1, [h2, c]] + # the first one must not be contained in fieldset + # so we make the tree not fillable + def fillable? + false + end + + def split_section_champ(node) + case node + when EditableChamp::SectionComponent + [node, nil] + else + [nil, node] + end + end + + private + + def to_fieldset(nodes:) + nodes.map { _1.is_a?(Array) ? EditableChamp::SectionComponent.new(nodes: _1) : _1 } + end + + def first_champ_is_an_header_section? + header_section.present? + end + + def any_champ_fillable? + tail.any? { _1&.fillable? } + end +end diff --git a/app/components/editable_champ/section_component/section_component.html.haml b/app/components/editable_champ/section_component/section_component.html.haml new file mode 100644 index 000000000..b6ecc682c --- /dev/null +++ b/app/components/editable_champ/section_component/section_component.html.haml @@ -0,0 +1,19 @@ +- if render_within_fieldset? + = tag.fieldset(class: "reset-#{tag_for_depth}") do + = tag.legend do + = render EditableChamp::HeaderSectionComponent.new(champ: header_section) + - splitted_tail.each do |section, champ| + - if section.present? + = render section + - else + = fields_for champ.input_name, champ do |form| + = render EditableChamp::EditableChampComponent.new form: ,champ: +- else + - if header_section + = render EditableChamp::HeaderSectionComponent.new(champ: header_section) + - splitted_tail.each do |section, champ| + - if section.present? + = render section + - else + = fields_for champ.input_name, champ do |form| + = render EditableChamp::EditableChampComponent.new form: ,champ: diff --git a/app/controllers/administrateurs/types_de_champ_controller.rb b/app/controllers/administrateurs/types_de_champ_controller.rb index 48dbf9d72..c77310dbd 100644 --- a/app/controllers/administrateurs/types_de_champ_controller.rb +++ b/app/controllers/administrateurs/types_de_champ_controller.rb @@ -128,7 +128,7 @@ module Administrateurs :drop_down_secondary_description, :collapsible_explanation_enabled, :collapsible_explanation_text, - :header_section_level + :header_section_level, editable_options: [ :cadastres, :unesco, diff --git a/app/models/concerns/treeable_concern.rb b/app/models/concerns/treeable_concern.rb index 3f269e11e..81592eff7 100644 --- a/app/models/concerns/treeable_concern.rb +++ b/app/models/concerns/treeable_concern.rb @@ -6,32 +6,32 @@ module TreeableConcern included do # as we progress in the list of ordered champs - # we keep a reference to each level of nesting (depth_cache) + # we keep a reference to each level of nesting (walk) # when we encounter an header_section, it depends of its own depth of nesting minus 1, ie: - # h1 belongs to prior (root) + # h1 belongs to prior (rooted_tree) # h2 belongs to prior h1 # h3 belongs to prior h2 - # h1 belongs to prior (root) + # h1 belongs to prior (rooted_tree) # then, each and every champs which are not an header_section - # are added to the most_recent_subtree + # are added to the current_tree # given a root_depth at 0, we build a full tree # given a root_depth > 0, we build a partial tree (aka, a repetition) - def to_tree(champs:, root_depth:) - root = [] - depth_cache = Array.new(MAX_DEPTH) - depth_cache[root_depth] = root - most_recent_subtree = root + def to_tree(champs:) + rooted_tree = [] + walk = Array.new(MAX_DEPTH) + walk[0] = rooted_tree + current_tree = rooted_tree champs.each do |champ| if champ.header_section? - champs_subtree = [champ] - depth_cache[champ.level - 1].push(champs_subtree) - most_recent_subtree = depth_cache[champ.level] = champs_subtree + new_tree = [champ] + walk[champ.header_section_level_value - 1].push(new_tree) + current_tree = walk[champ.header_section_level_value] = new_tree else - most_recent_subtree.push(champ) + current_tree.push(champ) end end - root + rooted_tree end end end diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index e3a4563bc..647d9ba16 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -42,5 +42,5 @@ = f.select :groupe_instructeur_id, dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] }, { include_blank: dossier.brouillon? } - = render EditableChamp::ChampsTreeComponent.new(champs: dossier.champs_public, root_depth: 0) + = render EditableChamp::SectionComponent.new(champs: dossier.champs_public) = render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: false) diff --git a/app/views/shared/dossiers/_edit_annotations.html.haml b/app/views/shared/dossiers/_edit_annotations.html.haml index 7f69a1390..5dfaefff0 100644 --- a/app/views/shared/dossiers/_edit_annotations.html.haml +++ b/app/views/shared/dossiers/_edit_annotations.html.haml @@ -3,7 +3,7 @@ %section.counter-start-header-section = render NestedForms::FormOwnerComponent.new = form_for dossier, url: annotations_instructeur_dossier_path(dossier.procedure, dossier), html: { class: 'form', multipart: true } do |f| - = render EditableChamp::ChampsTreeComponent.new(champs: dossier.champs_private, root_depth: 0) + = render EditableChamp::SectionComponent.new(champs: dossier.champs_private) = render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: true) - else diff --git a/spec/components/editable_champ/champs_subtree_component_spec.rb b/spec/components/editable_champ/section_component_spec.rb similarity index 72% rename from spec/components/editable_champ/champs_subtree_component_spec.rb rename to spec/components/editable_champ/section_component_spec.rb index 792aa7140..511fa7898 100644 --- a/spec/components/editable_champ/champs_subtree_component_spec.rb +++ b/spec/components/editable_champ/section_component_spec.rb @@ -1,8 +1,6 @@ -describe EditableChamp::ChampsSubtreeComponent, type: :component do +describe EditableChamp::SectionComponent, type: :component do include TreeableConcern - let(:component) { described_class.new(nodes: nodes) } - let(:nodes) { to_tree(champs:, root_depth:) } - let(:root_depth) { 0 } + let(:component) { described_class.new(champs: champs) } before { render_inline(component).to_html } context 'list of champs without an header_section' do @@ -83,4 +81,33 @@ describe EditableChamp::ChampsSubtreeComponent, type: :component do expect(page).to have_selector("fieldset fieldset textarea", count: 1) end end + + context 'with repetition' do + let(:procedure) do + create(:procedure, types_de_champ_public: [ + { type: :header_section, header_section_level: 1 }, + { + type: :repetition, + libelle: 'repetition', + children: [ + { type: :header_section, header_section_level: 1, libelle: 'child_1' }, + { type: :text, libelle: 'child_2' } + ] + } + ]) + end + let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) } + let(:champs) { dossier.champs_public } + + it 'render nested fieldsets, increase heading level for repetition header_section' do + expect(page).to have_selector("fieldset") + expect(page).to have_selector("legend h2") + expect(page).to have_selector("fieldset fieldset") + expect(page).to have_selector("fieldset fieldset legend h3") + end + + it 'contains as many text champ as repetition.rows' do + expect(page).to have_selector("fieldset fieldset input[type=text]", count: dossier.champs_public.find(&:repetition?).rows.size) + end + end end diff --git a/spec/models/concern/treeable_concern_spec.rb b/spec/models/concern/treeable_concern_spec.rb index 9887df4f3..d3f12d5d9 100644 --- a/spec/models/concern/treeable_concern_spec.rb +++ b/spec/models/concern/treeable_concern_spec.rb @@ -3,12 +3,12 @@ describe TreeableConcern do include TreeableConcern attr_reader :root - def initialize(champs:, root_depth:) - @root = to_tree(champs:, root_depth:) + def initialize(champs:) + @root = to_tree(champs:) end end - subject { ChampsToTree.new(champs: champs, root_depth: 0).root } + subject { ChampsToTree.new(champs: champs).root } describe "to_tree" do let(:header_1) { build(:champ_header_section_level_1) } let(:header_1_2) { build(:champ_header_section_level_2) } From 4a2e499679295d855f0e46f861debc0fac88cd76 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 20 Apr 2023 10:16:48 +0200 Subject: [PATCH 56/75] fix(commune): should accept postal codes with spaces --- app/models/champs/commune_champ.rb | 4 ++++ spec/models/champs/commune_champ_spec.rb | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models/champs/commune_champ.rb b/app/models/champs/commune_champ.rb index 20827d93c..06b6f51fe 100644 --- a/app/models/champs/commune_champ.rb +++ b/app/models/champs/commune_champ.rb @@ -52,6 +52,10 @@ class Champs::CommuneChamp < Champs::TextChamp code_postal.present? end + def code_postal=(value) + super(value&.gsub(/[[:space:]]/, '')) + end + alias postal_code code_postal def name diff --git a/spec/models/champs/commune_champ_spec.rb b/spec/models/champs/commune_champ_spec.rb index a2ade44b2..622e6fb67 100644 --- a/spec/models/champs/commune_champ_spec.rb +++ b/spec/models/champs/commune_champ_spec.rb @@ -2,10 +2,9 @@ describe Champs::CommuneChamp do let(:code_insee) { '63102' } let(:code_postal) { '63290' } let(:code_departement) { '63' } + let(:champ) { create(:champ_communes, code_postal: code_postal) } describe 'value' do - let(:champ) { create(:champ_communes, code_postal: code_postal) } - it 'with code_postal' do champ.update(value: code_insee) expect(champ.to_s).to eq('Châteldon (63290)') @@ -18,4 +17,12 @@ describe Champs::CommuneChamp do expect(champ.communes.size).to eq(8) end end + + describe 'code_postal with spaces' do + let(:code_postal) { ' 63 2 90  ' } + + it 'with code_postal' do + expect(champ.communes.size).to eq(8) + end + end end From 2db21c8789ae317eb24b41603b9f661703e820f6 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 20 Apr 2023 16:19:03 +0200 Subject: [PATCH 57/75] =?UTF-8?q?correctif(administrateurs/procedure#dossi?= =?UTF-8?q?er=5Fvide):=20affiche=203=20r=C3=A9p=C3=A9tition=20sur=20le=20f?= =?UTF-8?q?ormulaire=20pdf=20d'une=20d=C3=A9marche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/dossiers/dossier_vide.pdf.prawn | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/dossiers/dossier_vide.pdf.prawn b/app/views/dossiers/dossier_vide.pdf.prawn index 13224475e..a93376705 100644 --- a/app/views/dossiers/dossier_vide.pdf.prawn +++ b/app/views/dossiers/dossier_vide.pdf.prawn @@ -186,6 +186,7 @@ def add_champs(pdf, champs) champs.each do |champ| if champ.type == 'Champs::RepetitionChamp' add_libelle(pdf, champ) + 3.times { champ.add_row(champ.type_de_champ.revision) } 3.times do champ.rows.each do |row| row.each do |inner_champ| From 6739bfc6651ecd9f7ec377b1232bb6d12ef478e4 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 21 Apr 2023 09:35:16 +0200 Subject: [PATCH 58/75] =?UTF-8?q?bug(instructeurs.annotations):=20ETQ=20in?= =?UTF-8?q?structeurs,=20lorsque=20je=20saisie=20une=20autre=20valeur=20da?= =?UTF-8?q?ns=20un=20champ=20de=20type=20choix=20simple,=20celle=20ci=20n'?= =?UTF-8?q?est=20pas=20sauvegard=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instructeurs/dossiers_controller_spec.rb | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 06f94d716..4b499faf4 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -762,28 +762,19 @@ describe Instructeurs::DossiersController, type: :controller do { type: :multiple_drop_down_list }, { type: :linked_drop_down_list }, { type: :datetime }, - { type: :repetition, children: [{}] } + { type: :repetition, children: [{}] }, + { type: :drop_down_list, options: [:a, :b, :other] } ], instructeurs: instructeurs) end let(:dossier) { create(:dossier, :en_construction, :with_populated_annotations, procedure: procedure) } let(:another_instructeur) { create(:instructeur) } let(:now) { Time.zone.parse('01/01/2100') } - let(:champ_multiple_drop_down_list) do - dossier.champs_private.first - end - - let(:champ_linked_drop_down_list) do - dossier.champs_private.second - end - - let(:champ_datetime) do - dossier.champs_private.third - end - - let(:champ_repetition) do - dossier.champs_private.fourth - end + let(:champ_multiple_drop_down_list) { dossier.champs_private.first } + let(:champ_linked_drop_down_list) { dossier.champs_private.second } + let(:champ_datetime) { dossier.champs_private.third } + let(:champ_repetition) { dossier.champs_private.fourth } + let(:champ_drop_down_list) { dossier.champs_private.fifth } before do expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :annotations_privees) @@ -795,6 +786,7 @@ describe Instructeurs::DossiersController, type: :controller do champ_linked_drop_down_list.reload champ_datetime.reload champ_repetition.reload + champ_drop_down_list.reload end after do @@ -824,6 +816,11 @@ describe Instructeurs::DossiersController, type: :controller do '3': { id: champ_repetition.champs.first.id, value: 'text' + }, + '4': { + id: champ_drop_down_list.id, + value: '__other__', + value_other: 'other value' } } } @@ -836,6 +833,7 @@ describe Instructeurs::DossiersController, type: :controller do expect(champ_linked_drop_down_list.secondary_value).to eq('secondary') expect(champ_datetime.value).to eq('2019-12-21T13:17:00+01:00') expect(champ_repetition.champs.first.value).to eq('text') + expect(champ_drop_down_list.value).to eq('other value') expect(dossier.reload.last_champ_private_updated_at).to eq(now) expect(response).to have_http_status(200) } From 06de9bf4b40674618b2abca3dd3a2303f73619fb Mon Sep 17 00:00:00 2001 From: Eric Leroy-Terquem Date: Fri, 21 Apr 2023 09:56:32 +0200 Subject: [PATCH 59/75] do not use notifier verb in the wrong way --- .../administrateurs/procedures/_informations.html.haml | 3 +-- .../instructeurs/dossiers/_instruction_button.html.haml | 6 +++--- .../views/administrateurs/groupe_instructeurs/fr.yml | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/administrateurs/procedures/_informations.html.haml b/app/views/administrateurs/procedures/_informations.html.haml index 3054363fa..80d842e9e 100644 --- a/app/views/administrateurs/procedures/_informations.html.haml +++ b/app/views/administrateurs/procedures/_informations.html.haml @@ -122,7 +122,7 @@ = f.label :web_hook_url do Lien de rappel HTTP (webhook) %p.notice - Vous pouvez définir un lien de rappel HTTP (aussi appelé webhook) pour notifier un service tiers du changement de l'état d’un dossier de cette démarche sur #{APPLICATION_NAME}. + Vous pouvez définir un lien de rappel HTTP (aussi appelé webhook) pour informer un service tiers du changement de l'état d’un dossier de cette démarche sur #{APPLICATION_NAME}. = link_to("Consulter la documentation du webhook", WEBHOOK_DOC_URL, target: "_blank", rel: "noopener") = f.text_field :web_hook_url, class: 'form-control', placeholder: 'https://callback.exemple.fr/' @@ -151,4 +151,3 @@ Champ “Pièce justificative” avec multiples fichiers %p.notice Autorise les usagers à envoyer plusieurs fichiers pour les champs de type “Pièce justificative”. L'activation de cette option est irréversible et peut nécessiter des modifications si vous utilisez des systèmes automatisés pour traiter les dossiers. - diff --git a/app/views/instructeurs/dossiers/_instruction_button.html.haml b/app/views/instructeurs/dossiers/_instruction_button.html.haml index ef5adbfb1..e73a8fbf3 100644 --- a/app/views/instructeurs/dossiers/_instruction_button.html.haml +++ b/app/views/instructeurs/dossiers/_instruction_button.html.haml @@ -8,7 +8,7 @@ %span.icon.accept .dropdown-description %h4 Accepter - L’usager sera notifié que son dossier a été accepté + L’usager sera informé que son dossier a été accepté - menu.with_item(class: "hidden inactive form-inside") do = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est accepté (facultatif)', popup_class: 'accept', process_action: 'accepter', title: 'Accepter', confirm: "Confirmez-vous l'acceptation ce dossier ?" } @@ -19,7 +19,7 @@ %span.icon.without-continuation .dropdown-description %h4 Classer sans suite - L’usager sera notifié que son dossier a été classé sans suite + L’usager sera informé que son dossier a été classé sans suite - menu.with_item(class: "hidden inactive form-inside") do = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est classé sans suite (obligatoire)', popup_class: 'without-continuation', process_action: 'classer_sans_suite', title: 'Classer sans suite', confirm: 'Confirmez-vous le classement sans suite de ce dossier ?' } @@ -29,7 +29,7 @@ %span.icon.refuse .dropdown-description %h4 Refuser - L’usager sera notifié que son dossier a été refusé + L’usager sera informé que son dossier a été refusé - menu.with_item(class: "hidden inactive form-inside") do = render partial: 'instructeurs/dossiers/instruction_button_motivation', locals: { dossier: dossier, placeholder: 'Expliquez au demandeur pourquoi ce dossier est refusé (obligatoire)', popup_class: 'refuse', process_action: 'refuser', title: 'Refuser', confirm: 'Confirmez-vous le refus de ce dossier ?' } diff --git a/config/locales/views/administrateurs/groupe_instructeurs/fr.yml b/config/locales/views/administrateurs/groupe_instructeurs/fr.yml index 3193788ab..dc49c86b6 100644 --- a/config/locales/views/administrateurs/groupe_instructeurs/fr.yml +++ b/config/locales/views/administrateurs/groupe_instructeurs/fr.yml @@ -53,7 +53,7 @@ fr: notice_2: L’import n’écrase pas les instructeurs existants. Il permet uniquement d'en ajouter. Pour supprimer un instructeur, cliquez sur le bouton « retirer ». Le poids du fichier doit être inférieur %{csv_max_size}. import_file: Importer le fichier import_file_procedure_not_published: L’import d’instructeurs par fichier CSV est disponible une fois la démarche publiée - import_file_alert: Tous les instructeurs ajoutés à la procédure vont être notifiés par email. Voulez-vous continuer ? + import_file_alert: Tous les instructeurs ajoutés à la procédure vont être informés par email. Voulez-vous continuer ? link_text: exemple de fichier instructeurs_file_path: /csv/import-instructeurs-test.csv groupes_file_path: /csv/fr/import-groupe-test.csv From b5e6c3d02a6fd894c0ef265bc045db1abd7ebafd Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 21 Apr 2023 09:37:27 +0200 Subject: [PATCH 60/75] =?UTF-8?q?correctif(instructeurs.annotations):=20po?= =?UTF-8?q?ur=20un=20champ=20de=20type=20choix=20simple,=20l'ordre=20des?= =?UTF-8?q?=20parametres=20value,=20value=5Fother=20doit=20etre=20respect?= =?UTF-8?q?=C3=A9=20sinon=20le=20'setters'=20`Champs::DropDownListChamp.va?= =?UTF-8?q?lue=3D`=20passe=20apr=C3=A8s=20le=20setter=20`Champs::DropDownL?= =?UTF-8?q?istChamp.value=5Fother=3D`,=20ce=20qui=20a=20pour=20effet=20de?= =?UTF-8?q?=20supprimer=20la=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/instructeurs/dossiers_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index a5997544a..47c930eae 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -326,7 +326,7 @@ module Instructeurs def champs_private_params champs_params = params.require(:dossier).permit(champs_private_attributes: [ - :id, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, :value, value: [], + :id, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, value: [], champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, value: []] ]) champs_params[:champs_private_all_attributes] = champs_params.delete(:champs_private_attributes) || {} From 52d63a1b02eadbe5d5c5dee77bb07380ede27247 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 21 Apr 2023 11:18:27 +0100 Subject: [PATCH 61/75] fix(dossier): user can create new dossiers when closed procedure is replaced --- app/models/procedure.rb | 4 ++++ app/views/users/dossiers/_dossier_actions.html.haml | 2 +- spec/views/users/dossiers/_dossier_actions.html.haml_spec.rb | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 1519105c2..a56d50fad 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -453,6 +453,10 @@ class Procedure < ApplicationRecord publiee? || brouillon? end + def replaced_by_procedure? + replaced_by_procedure_id.present? + end + def dossier_can_transition_to_en_construction? accepts_new_dossiers? || depubliee? end diff --git a/app/views/users/dossiers/_dossier_actions.html.haml b/app/views/users/dossiers/_dossier_actions.html.haml index 2afd45f93..1b14d428b 100644 --- a/app/views/users/dossiers/_dossier_actions.html.haml +++ b/app/views/users/dossiers/_dossier_actions.html.haml @@ -1,6 +1,6 @@ - has_edit_action = !dossier.read_only? - has_delete_action = dossier.can_be_deleted_by_user? -- has_new_dossier_action = dossier.procedure.accepts_new_dossiers? +- has_new_dossier_action = dossier.procedure.accepts_new_dossiers? || dossier.procedure.replaced_by_procedure? - has_transfer_action = dossier.user == current_user - has_actions = has_edit_action || has_delete_action || has_new_dossier_action || has_transfer_action diff --git a/spec/views/users/dossiers/_dossier_actions.html.haml_spec.rb b/spec/views/users/dossiers/_dossier_actions.html.haml_spec.rb index 339e3b973..82bc7e677 100644 --- a/spec/views/users/dossiers/_dossier_actions.html.haml_spec.rb +++ b/spec/views/users/dossiers/_dossier_actions.html.haml_spec.rb @@ -18,4 +18,9 @@ describe 'users/dossiers/dossier_actions.html.haml', type: :view do let(:procedure) { create(:procedure, :closed) } it { is_expected.not_to have_link('Commencer un autre dossier') } end + + context 'when the procedure is closed and replaced' do + let(:procedure) { create(:procedure, :closed, replaced_by_procedure: create(:procedure)) } + it { is_expected.to have_link('Commencer un autre dossier') } + end end From a202a579887ba0bf5149b2e3bce0695aa3b4ae9c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 21 Apr 2023 11:19:09 +0200 Subject: [PATCH 62/75] fix: defaut_groupe_instructeur_id for a cloned procedure --- app/models/procedure.rb | 3 +++ spec/models/procedure_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 1519105c2..91e34bd70 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -559,6 +559,9 @@ class Procedure < ApplicationRecord procedure.draft_revision.types_de_champ_public.each { |tdc| tdc.options&.delete(:old_pj) } end + new_defaut_groupe = procedure.groupe_instructeurs.find_by(label: defaut_groupe_instructeur.label) + procedure.update!(defaut_groupe_instructeur: new_defaut_groupe) + procedure end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 9a8260720..71fdc85f8 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -518,6 +518,7 @@ describe Procedure do expect(subject.groupe_instructeurs.size).to eq(2) expect(subject.groupe_instructeurs.size).to eq(procedure.groupe_instructeurs.size) expect(subject.groupe_instructeurs.where(label: "groupe_1").first).not_to be nil + expect(subject.defaut_groupe_instructeur_id).to eq(subject.groupe_instructeurs.find_by(label: 'défaut').id) end it "should clone instructeurs in the groupe" do @@ -568,7 +569,8 @@ describe Procedure do cloned_procedure.parent_procedure_id = nil expect(cloned_procedure).to have_same_attributes_as(procedure, except: [ "path", "draft_revision_id", "service_id", 'estimated_dossiers_count', - "duree_conservation_etendue_par_ds", "duree_conservation_dossiers_dans_ds", 'max_duree_conservation_dossiers_dans_ds' + "duree_conservation_etendue_par_ds", "duree_conservation_dossiers_dans_ds", 'max_duree_conservation_dossiers_dans_ds', + "defaut_groupe_instructeur_id" ]) end From 83b8570a6568842710d4a37627b67d0771292292 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 21 Apr 2023 11:42:44 +0200 Subject: [PATCH 63/75] fix: add rake task to fix wrong defaut_groupe_instructeur to instance: skip this task if you merge this release with 2023-04-17-01 --- ...e_instructeur_id_for_cloned_procedure.rake | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake diff --git a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake new file mode 100644 index 000000000..7ddaee105 --- /dev/null +++ b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake @@ -0,0 +1,19 @@ +namespace :after_party do + desc 'Deployment task: fix_defaut_groupe_instructeur_id_for_cloned_procedure' + task fix_defaut_groupe_instructeur_id_for_cloned_procedure: :environment do + procedures = Procedure + .joins(:groupe_instructeurs) + .where.not(parent_procedure_id: nil) + .where("procedures.created_at > ?", Time.zone.parse("17/04/2023")) + + procedures.each do |p| + if !p.defaut_groupe_instructeur_id.in?(p.groupe_instructeurs.map(&:id)) + new_defaut_groupe = p.groupe_instructeurs.find_by(label: p.parent_procedure.defaut_groupe_instructeur.label) + p.update!(defaut_groupe_instructeur: new_defaut_groupe) + end + end + + AfterParty::TaskRecord + .create version: AfterParty::TaskRecorder.new(__FILE__).timestamp + end +end From 1b0285766210db5c9693fa93a79617e23029884c Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Fri, 21 Apr 2023 12:50:09 +0200 Subject: [PATCH 64/75] add pagination title and add missing DSFR style on table --- .../dossiers/_deleted_dossiers_list.html.haml | 45 +++++++++------- .../users/dossiers/_dossiers_list.html.haml | 4 +- .../dossiers/_hidden_dossiers_list.html.haml | 54 ++++++++++--------- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml index 55785b475..23df15f00 100644 --- a/app/views/users/dossiers/_deleted_dossiers_list.html.haml +++ b/app/views/users/dossiers/_deleted_dossiers_list.html.haml @@ -1,26 +1,31 @@ - if deleted_dossiers.present? - %table.table.dossiers-table.hoverable - %thead - %tr - %th.number-col Nº dossier - %th Démarche - %th Raison de suppression - %th Date de suppression - %tbody - - deleted_dossiers.each do |dossier| - - libelle_demarche = Procedure.find(dossier.procedure_id).libelle + %span.fr-h6.fr-mr-2w + = page_entries_info hidden_dossiers - %tr{ data: { 'dossier-id': dossier.dossier_id } } - %td.number-col - %span.icon.folder - = dossier.dossier_id - %td - = libelle_demarche + .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| + - libelle_demarche = Procedure.find(dossier.procedure_id).libelle - %td.cell-link - = deletion_reason_badge(dossier.reason) - %td - = dossier.updated_at.strftime('%d/%m/%Y') + %tr{ data: { 'dossier-id': dossier.dossier_id } } + %td.number-col + %span.icon.folder + = dossier.dossier_id + %td + = libelle_demarche + + %td.cell-link + = deletion_reason_badge(dossier.reason) + %td + = dossier.updated_at.strftime('%d/%m/%Y') = paginate(deleted_dossiers) diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index 0d4f0d3b0..26ba0d038 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -1,5 +1,7 @@ - if dossiers.present? - .fr-table.fr-table--bordered + %span.fr-h6.fr-mr-2w + = page_entries_info 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 diff --git a/app/views/users/dossiers/_hidden_dossiers_list.html.haml b/app/views/users/dossiers/_hidden_dossiers_list.html.haml index 58f2f7eb1..0cf10f3dd 100644 --- a/app/views/users/dossiers/_hidden_dossiers_list.html.haml +++ b/app/views/users/dossiers/_hidden_dossiers_list.html.haml @@ -1,31 +1,37 @@ - if hidden_dossiers.present? - %table.table.dossiers-table.hoverable - %thead - %tr - %th.number-col Nº dossier - %th Démarche - %th Raison de suppression - %th Date de suppression - %tbody - - hidden_dossiers.each do |dossier| - - libelle_demarche = dossier.procedure.libelle + %span.fr-h6.fr-mr-2w + = page_entries_info hidden_dossiers - %tr{ data: { 'dossier-id': dossier.id } } - %td.number-col - %span.icon.folder - = dossier.id - %td - = libelle_demarche + .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 - %td.cell-link - = deletion_reason_badge("user_request") - %td - = dossier.updated_at.strftime('%d/%m/%Y') - %td - = link_to restore_dossier_path(dossier.id), method: :patch, class: "button primary" do - Restaurer + %tr{ data: { 'dossier-id': dossier.id } } + %td.number-col + %span.icon.folder + = dossier.id + %td + = libelle_demarche - = paginate(hidden_dossiers) + %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) - else .blank-tab From 446ff5f28fbb0c2e90ce6d7afe05543086bbe21e Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Fri, 21 Apr 2023 13:04:19 +0200 Subject: [PATCH 65/75] feat(manager): edit procedure#replaced_by_procedure_id --- app/dashboards/procedure_dashboard.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/dashboards/procedure_dashboard.rb b/app/dashboards/procedure_dashboard.rb index ee76168bd..04bfc5fa8 100644 --- a/app/dashboards/procedure_dashboard.rb +++ b/app/dashboards/procedure_dashboard.rb @@ -41,6 +41,7 @@ class ProcedureDashboard < Administrate::BaseDashboard max_duree_conservation_dossiers_dans_ds: Field::Number, estimated_duration_visible: Field::Boolean, piece_justificative_multiple: Field::Boolean, + replaced_by_procedure_id: Field::String, tags: Field::Text }.freeze @@ -95,7 +96,8 @@ class ProcedureDashboard < Administrate::BaseDashboard :duree_conservation_dossiers_dans_ds, :max_duree_conservation_dossiers_dans_ds, :estimated_duration_visible, - :piece_justificative_multiple + :piece_justificative_multiple, + :replaced_by_procedure_id ].freeze # FORM_ATTRIBUTES @@ -106,7 +108,8 @@ class ProcedureDashboard < Administrate::BaseDashboard :duree_conservation_dossiers_dans_ds, :max_duree_conservation_dossiers_dans_ds, :estimated_duration_visible, - :piece_justificative_multiple + :piece_justificative_multiple, + :replaced_by_procedure_id ].freeze # Overwrite this method to customize how procedures are displayed From fb54ca1469594f564cd291d61dc4fd0441b66f96 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Thu, 20 Apr 2023 13:10:45 +0200 Subject: [PATCH 66/75] fix(markdown): don't convert underscore within urls or emails --- app/components/simple_format_component.rb | 14 ++++++++-- app/lib/redcarpet/bare_renderer.rb | 1 + .../simple_format_component_spec.rb | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/components/simple_format_component.rb b/app/components/simple_format_component.rb index c9f6f8199..74819a002 100644 --- a/app/components/simple_format_component.rb +++ b/app/components/simple_format_component.rb @@ -21,12 +21,22 @@ class SimpleFormatComponent < ApplicationComponent no_images: true } + SIMPLE_URL_REGEX = %r{https?://\S+} + EMAIL_IN_TEXT_REGEX = Regexp.new(Devise.email_regexp.source.gsub(/\\A|\\z/, '\b')) + def initialize(text, allow_a: true, class_names_map: {}) + @allow_a = allow_a + @text = (text || "").gsub(/\R/, "\n\n") # force double \n otherwise a single one won't split paragraph .split("\n\n") # .map(&:lstrip) # this block prevent redcarpet to consider " text" as block code by lstriping - .join("\n\n") # - @allow_a = allow_a + .join("\n\n") + .gsub(EMAIL_IN_TEXT_REGEX) { _1.gsub('_', '\\_') } # Workaround for redcarpet bug on autolink email having _. Cf tests + + if !@allow_a + @text = @text.gsub(SIMPLE_URL_REGEX) { _1.gsub('_', '\\_') } # Escape underscores in URLs + end + @renderer = Redcarpet::Markdown.new( Redcarpet::BareRenderer.new(class_names_map:), REDCARPET_EXTENSIONS.merge(autolink: @allow_a) diff --git a/app/lib/redcarpet/bare_renderer.rb b/app/lib/redcarpet/bare_renderer.rb index e672ee39a..5e4e9850a 100644 --- a/app/lib/redcarpet/bare_renderer.rb +++ b/app/lib/redcarpet/bare_renderer.rb @@ -26,6 +26,7 @@ module Redcarpet when :url link(link, nil, link) when :email + # NOTE: As of Redcarpet 3.6.0, autolinking email containing is broken https://github.com/vmg/redcarpet/issues/402 content_tag(:a, link, { href: "mailto:#{link}" }) else link diff --git a/spec/components/simple_format_component_spec.rb b/spec/components/simple_format_component_spec.rb index 2028c8b52..dc6cf6cbb 100644 --- a/spec/components/simple_format_component_spec.rb +++ b/spec/components/simple_format_component_spec.rb @@ -90,4 +90,31 @@ TEXT it { expect(page).not_to have_selector("a") } end end + + context 'emphasis not in urls' do + let(:text) do + <<~TEXT + A _string emphased_ but https://example.fr/path_preserves_underscore + email: here_is_my@email.com + TEXT + end + + context "without autolink" do + let(:allow_a) { false } + it { expect(page).to have_selector("em", count: 1, text: "string emphased") } + it { expect(page).to have_text("https://example.fr/path_preserves_underscore") } + it { expect(page).to have_text("email: here_is_my@email.com") } + end + + context "with autolink" do + let(:allow_a) { true } + it { + expect(page).to have_link("https://example.fr/path_preserves_underscore") + + # NOTE: As of Redcarpet 3.6.0, autolinking email containing _ is broken https://github.com/vmg/redcarpet/issues/402 + # but we still want the email to be displayed + expect(page).to have_text("here_is_my@email.com") + } + end + end end From 439447b3433bd980bd740f428b7558a56cbee7c3 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 21 Apr 2023 15:07:33 +0200 Subject: [PATCH 67/75] fix defaut_groupe_instructeur_id and change dossier.groupe_instructeur_id --- ...e_instructeur_id_for_cloned_procedure.rake | 3 +++ ...tructeur_id_for_cloned_procedure_spec.rake | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake diff --git a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake index 7ddaee105..16fef0c4a 100644 --- a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake +++ b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake @@ -8,8 +8,11 @@ namespace :after_party do procedures.each do |p| if !p.defaut_groupe_instructeur_id.in?(p.groupe_instructeurs.map(&:id)) + wrong_groupe = p.defaut_groupe_instructeur new_defaut_groupe = p.groupe_instructeurs.find_by(label: p.parent_procedure.defaut_groupe_instructeur.label) p.update!(defaut_groupe_instructeur: new_defaut_groupe) + + p.dossiers.where(groupe_instructeur: wrong_groupe).update_all(groupe_instructeur_id: new_defaut_groupe.id) end end diff --git a/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake b/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake new file mode 100644 index 000000000..f5add3d77 --- /dev/null +++ b/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake @@ -0,0 +1,26 @@ +describe '20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure' do + let(:rake_task) { Rake::Task['after_party:fix_defaut_groupe_instructeur_id_for_cloned_procedure'] } + + let!(:parent_procedure) { create(:procedure) } + let(:procedure) { create(:procedure, parent_procedure:) } + let(:dossier) { create(:dossier, procedure:) } + + subject(:run_task) { rake_task.invoke } + after(:each) { rake_task.reenable } + + it 'populates defaut_groupe_instructeur_id' do + procedure.update_columns(defaut_groupe_instructeur_id: parent_procedure.defaut_groupe_instructeur_id) + dossier + + expect(procedure.defaut_groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be false + expect(dossier.groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be false + + run_task + + procedure.reload + dossier.reload + + expect(procedure.defaut_groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be true + expect(dossier.groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be true + end +end From 9dec6f161144f50f5037b4a0d9d7f2f1cd924366 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 21 Apr 2023 11:40:23 +0200 Subject: [PATCH 68/75] =?UTF-8?q?correctif(procedure.declarative):=20ETQ?= =?UTF-8?q?=20administrateur=20d'une=20procedure=20declarative,=20certains?= =?UTF-8?q?=20de=20mes=20dossiers=20restent=20en=20construction=20[ex:=20l?= =?UTF-8?q?'object=20storage=20est=20down,=20le=20dossier=20reste=20bloqu?= =?UTF-8?q?=C3=A9]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stalled_declarative_procedures_job.rb | 14 ++ app/models/procedure.rb | 17 +++ ...stalled_declarative_procedures_job_spec.rb | 126 ++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 app/jobs/cron/stalled_declarative_procedures_job.rb create mode 100644 spec/jobs/cron/stalled_declarative_procedures_job_spec.rb diff --git a/app/jobs/cron/stalled_declarative_procedures_job.rb b/app/jobs/cron/stalled_declarative_procedures_job.rb new file mode 100644 index 000000000..c713bac0e --- /dev/null +++ b/app/jobs/cron/stalled_declarative_procedures_job.rb @@ -0,0 +1,14 @@ +class Cron::StalledDeclarativeProceduresJob < Cron::CronJob + self.schedule_expression = "every 10 minute" + + def perform(*args) + Procedure.declarative.find_each do |procedure| + begin + procedure.process_stalled_dossiers! + rescue => e + Sentry.set_tags(procedure: procedure.id) + Sentry.capture_exception(e) + end + end + end +end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 1519105c2..8a089571f 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -479,6 +479,23 @@ class Procedure < ApplicationRecord end end + def process_stalled_dossiers! + case declarative_with_state + when Procedure.declarative_with_states.fetch(:en_instruction) + dossiers + .state_en_construction + .where(declarative_triggered_at: nil) + .find_each(&:passer_automatiquement_en_instruction!) + when Procedure.declarative_with_states.fetch(:accepte) + dossiers + .state_en_construction + .where(declarative_triggered_at: nil) + .find_each do |dossier| + dossier.accepter_automatiquement! if dossier.can_accepter_automatiquement? + end + end + end + def feature_enabled?(feature) Flipper.enabled?(feature, self) end diff --git a/spec/jobs/cron/stalled_declarative_procedures_job_spec.rb b/spec/jobs/cron/stalled_declarative_procedures_job_spec.rb new file mode 100644 index 000000000..2d2f359aa --- /dev/null +++ b/spec/jobs/cron/stalled_declarative_procedures_job_spec.rb @@ -0,0 +1,126 @@ +RSpec.describe Cron::StalledDeclarativeProceduresJob, type: :job do + describe "perform" do + let(:date) { Time.utc(2017, 9, 1, 10, 5, 0) } + let(:instruction_date) { date + 120 } + + let(:state) { nil } + let(:procedure) { create(:procedure, :published, :for_individual, :with_instructeur, declarative_with_state: state) } + let(:nouveau_dossier1) { create(:dossier, :en_construction, :with_individual, :with_attestation, procedure: procedure) } + let(:nouveau_dossier2) { create(:dossier, :en_construction, :with_individual, :with_attestation, procedure: procedure) } + let(:dossier_recu) { create(:dossier, :en_instruction, :with_individual, procedure: procedure) } + let(:dossier_brouillon) { create(:dossier, procedure: procedure) } + let(:dossier_repasse_en_construction) { create(:dossier, :en_construction, :with_individual, procedure: procedure) } + + before do + Timecop.freeze(date) + dossier_repasse_en_construction&.touch(:declarative_triggered_at) + end + + subject(:perform_job) do + dossiers = [ + nouveau_dossier1, + nouveau_dossier2, + dossier_recu, + dossier_brouillon, + dossier_repasse_en_construction + ].compact + + Cron::StalledDeclarativeProceduresJob.new.perform + + dossiers.each(&:reload) + end + + after { Timecop.return } + + context "with some dossiers" do + context "en_construction" do + let(:state) { Dossier.states.fetch(:en_instruction) } + let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last } + + it { + perform_job + expect(nouveau_dossier1.en_instruction?).to be_truthy + expect(nouveau_dossier1.en_instruction_at).to eq(date) + expect(last_operation.operation).to eq('passer_en_instruction') + + expect(last_operation.automatic_operation?).to be_truthy + + expect(nouveau_dossier2.en_instruction?).to be_truthy + expect(nouveau_dossier2.en_instruction_at).to eq(date) + + expect(dossier_recu.en_instruction?).to be_truthy + expect(dossier_recu.en_instruction_at).to eq(instruction_date) + + expect(dossier_brouillon.brouillon?).to be_truthy + expect(dossier_brouillon.en_instruction_at).to eq(nil) + + expect(dossier_repasse_en_construction.en_construction?).to be_truthy + } + end + + context "accepte" do + let(:state) { Dossier.states.fetch(:accepte) } + let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last } + + it { + perform_job + expect(nouveau_dossier1.accepte?).to be true + expect(nouveau_dossier1.en_instruction_at).to eq(date) + expect(nouveau_dossier1.processed_at).to eq(date) + expect(nouveau_dossier1.attestation).to be_present + expect(last_operation.operation).to eq('accepter') + expect(last_operation.automatic_operation?).to be_truthy + + expect(nouveau_dossier2.accepte?).to be true + expect(nouveau_dossier2.en_instruction_at).to eq(date) + expect(nouveau_dossier2.processed_at).to eq(date) + expect(nouveau_dossier2.attestation).to be_present + + expect(dossier_recu.en_instruction?).to be true + expect(dossier_recu.en_instruction_at).to eq(instruction_date) + expect(dossier_recu.processed_at).to eq(nil) + + expect(dossier_brouillon.brouillon?).to be true + expect(dossier_brouillon.en_instruction_at).to eq(nil) + expect(dossier_brouillon.processed_at).to eq(nil) + } + + context "having etablissement in degraded_mode" do + let(:procedure) { create(:procedure, :published, :with_instructeur, for_individual: false, declarative_with_state: state) } + + let(:nouveau_dossier1) { create(:dossier, :en_construction, :with_entreprise, :with_attestation, procedure: procedure, as_degraded_mode: false) } + let(:nouveau_dossier2) { create(:dossier, :en_construction, :with_entreprise, :with_attestation, procedure: procedure, as_degraded_mode: true) } + let(:dossier_recu) { nil } + let(:dossier_repasse_en_construction) { nil } + + before do + expect(nouveau_dossier2).to_not receive(:accepter_automatiquement!) + expect(Sentry).to_not receive(:capture_exception) + end + + it { + perform_job + expect(nouveau_dossier1).to be_accepte + expect(nouveau_dossier2).to be_en_construction + } + end + end + end + end + + describe 'safer perform' do + let(:state) { Dossier.states.fetch(:en_instruction) } + + it 'works no matter if one raise' do + procedure_1 = instance_double("Procedure", id: 1) + expect(procedure_1).to receive(:process_stalled_dossiers!) + procedure_2 = instance_double("Procedure", id: 2) + expect(procedure_2).to receive(:process_stalled_dossiers!).and_raise("boom") + procedure_3 = double(process_stalled_dossiers!: true) + expect(procedure_3).to receive(:process_stalled_dossiers!) + + expect(Procedure).to receive_message_chain(:declarative, :find_each).and_yield(procedure_1).and_yield(procedure_2).and_yield(procedure_3) + Cron::StalledDeclarativeProceduresJob.perform_now + end + end +end From 1c20a9160c1cfdfcceac7f2ea3bbe995f0bda529 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Fri, 21 Apr 2023 17:19:17 +0200 Subject: [PATCH 69/75] add dsfr style pagination for all users --- app/views/administrate/application/_pagination.html.haml | 2 +- .../administrateurs/groupe_instructeurs/_edit.html.haml | 2 +- .../groupe_instructeurs/_instructeurs.html.haml | 2 +- .../groupe_instructeurs/reaffecter_dossiers.html.haml | 2 +- .../administrateurs/procedures/administrateurs.html.haml | 4 ++-- app/views/administrateurs/procedures/all.html.haml | 4 ++-- app/views/administrateurs/procedures/index.html.haml | 8 ++++---- app/views/experts/avis/procedure.html.haml | 2 +- .../instructeurs/groupe_instructeurs/index.html.haml | 2 +- app/views/instructeurs/groupe_instructeurs/show.html.haml | 2 +- .../instructeurs/procedures/deleted_dossiers.html.haml | 4 ++-- app/views/instructeurs/procedures/show.html.haml | 3 ++- app/views/recherche/index.html.haml | 4 ++-- .../kaminari/_first_page.html.haml | 0 .../{administrateurs => shared}/kaminari/_gap.html.haml | 0 .../kaminari/_last_page.html.haml | 0 .../kaminari/_next_page.html.haml | 0 .../{administrateurs => shared}/kaminari/_page.html.haml | 0 .../kaminari/_paginator.html.haml | 2 +- .../kaminari/_prev_page.html.haml | 0 app/views/users/dossiers/_deleted_dossiers_list.html.haml | 2 +- app/views/users/dossiers/_dossiers_list.html.haml | 2 +- app/views/users/dossiers/_hidden_dossiers_list.html.haml | 2 +- .../users/dossiers/_transfered_dossiers_list.html.haml | 2 +- 24 files changed, 26 insertions(+), 25 deletions(-) rename app/views/{administrateurs => shared}/kaminari/_first_page.html.haml (100%) rename app/views/{administrateurs => shared}/kaminari/_gap.html.haml (100%) rename app/views/{administrateurs => shared}/kaminari/_last_page.html.haml (100%) rename app/views/{administrateurs => shared}/kaminari/_next_page.html.haml (100%) rename app/views/{administrateurs => shared}/kaminari/_page.html.haml (100%) rename app/views/{administrateurs => shared}/kaminari/_paginator.html.haml (82%) rename app/views/{administrateurs => shared}/kaminari/_prev_page.html.haml (100%) diff --git a/app/views/administrate/application/_pagination.html.haml b/app/views/administrate/application/_pagination.html.haml index 16cc79876..6160626fb 100644 --- a/app/views/administrate/application/_pagination.html.haml +++ b/app/views/administrate/application/_pagination.html.haml @@ -3,4 +3,4 @@ = link_to_prev_page resources, t("views.pagination.previous"), param_name: "_page" = link_to_next_page resources, t("views.pagination.next"), param_name: "_page" - else - = paginate resources, param_name: "_page" + = paginate resources, param_name: "_page", views_prefix: 'shared' diff --git a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml index b5ea5e640..fdf5e95f5 100644 --- a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml @@ -64,4 +64,4 @@ = t('.group_management.move_files', count: group.dossiers.visible_by_administration.size) - = paginate groupes_instructeurs + = paginate groupes_instructeurs, views_prefix: 'shared' diff --git a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml index b5ec688d7..bc2a07f12 100644 --- a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml @@ -39,4 +39,4 @@ params: { instructeur: { id: instructeur.id }}, class: 'fr-btn fr-btn--secondary' } - = paginate instructeurs + = paginate instructeurs, views_prefix: 'shared' diff --git a/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml b/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml index 930628eba..7a54ec124 100644 --- a/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/reaffecter_dossiers.html.haml @@ -23,4 +23,4 @@ { class: 'button', data: { confirm: "Êtes-vous sûr de vouloir réaffecter les dossiers du groupe « #{@groupe_instructeur.label} » vers le groupe  « #{group.label} » ?" } } - = paginate @groupes_instructeurs + = paginate @groupes_instructeurs, views_prefix: 'shared' diff --git a/app/views/administrateurs/procedures/administrateurs.html.haml b/app/views/administrateurs/procedures/administrateurs.html.haml index de7daa525..5f2369d5c 100644 --- a/app/views/administrateurs/procedures/administrateurs.html.haml +++ b/app/views/administrateurs/procedures/administrateurs.html.haml @@ -29,7 +29,7 @@ - if @filter.from_publication_date.present? .selected-from-publication-date.fr-mb-2w = link_to "Depuis le #{l(@filter.from_publication_date)}", all_admin_procedures_path(@filter.without(:from_publication_date)), class: 'fr-tag fr-tag--dismiss fr-mb-1w' - = paginate @admins, views_prefix: 'administrateurs' + = paginate @admins, views_prefix: 'shared' %thead %tr %th{ scope: 'col' } @@ -51,4 +51,4 @@ %ul - admin.procedures.each do |procedure| %li= procedure.libelle - .fr-mt-2w= paginate @admins, views_prefix: 'administrateurs' + .fr-mt-2w= paginate @admins, views_prefix: 'shared' diff --git a/app/views/administrateurs/procedures/all.html.haml b/app/views/administrateurs/procedures/all.html.haml index 65d136b1c..0032d085f 100644 --- a/app/views/administrateurs/procedures/all.html.haml +++ b/app/views/administrateurs/procedures/all.html.haml @@ -38,7 +38,7 @@ - if @filter.from_publication_date.present? .selected-from-publication-date.fr-mb-2w = link_to "Depuis #{l(@filter.from_publication_date)}", all_admin_procedures_path(@filter.without(:from_publication_date)), class: 'fr-tag fr-tag--dismiss fr-mb-1w' - = paginate @procedures, params: @filter.params, views_prefix: 'administrateurs' + = paginate @procedures, params: @filter.params, , views_prefix: 'shared' %thead %tr %th{ scope: 'col' } @@ -52,4 +52,4 @@ %tbody{ 'data-turbo': 'true' } - @procedures.each do |procedure| = render partial: 'detail', locals: { procedure: procedure, show_detail: false } - .fr-mt-2w= paginate @procedures, params: @filter.params, views_prefix: 'administrateurs' + .fr-mt-2w= paginate @procedures, params: @filter.params, views_prefix: 'shared' diff --git a/app/views/administrateurs/procedures/index.html.haml b/app/views/administrateurs/procedures/index.html.haml index b29e49b81..2b883791c 100644 --- a/app/views/administrateurs/procedures/index.html.haml +++ b/app/views/administrateurs/procedures/index.html.haml @@ -15,16 +15,16 @@ .fr-container#procedures{ data: { item_count: @statut === "publiees" ? @procedures_publiees_count : @statut === "brouillons" ? @procedures_draft_count : @procedures_closed_count } } - if @statut === "publiees" = render partial: "procedures_list", locals: { procedures: @procedures_publiees } - = paginate @procedures_publiees + = paginate @procedures_publiees, views_prefix: 'shared' - if @statut === "brouillons" = render partial: "procedures_list", locals: { procedures: @procedures_draft } - = paginate @procedures_draft + = paginate @procedures_draft, views_prefix: 'shared' - if @statut === "archivees" = render partial: "procedures_list", locals: { procedures: @procedures_closed } - = paginate @procedures_closed + = paginate @procedures_closed, views_prefix: 'shared' - if @statut === "supprimees" = render partial: "procedures_list", locals: { procedures: @procedures_deleted } - = paginate @procedures_deleted + = paginate @procedures_deleted, views_prefix: 'shared' diff --git a/app/views/experts/avis/procedure.html.haml b/app/views/experts/avis/procedure.html.haml index 547d633f9..c8fc2f1cb 100644 --- a/app/views/experts/avis/procedure.html.haml +++ b/app/views/experts/avis/procedure.html.haml @@ -41,6 +41,6 @@ #{avis.dossier.id} %td= link_to(avis.dossier.user_email_for(:display), expert_avis_path(avis.procedure, avis), class: 'cell-link') %td= link_to(avis.procedure.libelle, expert_avis_path(avis.procedure, avis), class: 'cell-link') - = paginate(@avis) + = paginate @avis, views_prefix: 'shared' - else %h2.empty-text Aucun avis diff --git a/app/views/instructeurs/groupe_instructeurs/index.html.haml b/app/views/instructeurs/groupe_instructeurs/index.html.haml index 6f4a28f12..2cac708f4 100644 --- a/app/views/instructeurs/groupe_instructeurs/index.html.haml +++ b/app/views/instructeurs/groupe_instructeurs/index.html.haml @@ -17,4 +17,4 @@ %td= group.label %td.actions= link_to "voir", instructeur_groupe_path(@procedure, group) - = paginate @groupes_instructeurs + = paginate @groupes_instructeurs, views_prefix: 'shared' diff --git a/app/views/instructeurs/groupe_instructeurs/show.html.haml b/app/views/instructeurs/groupe_instructeurs/show.html.haml index 873adfca6..ef16232c2 100644 --- a/app/views/instructeurs/groupe_instructeurs/show.html.haml +++ b/app/views/instructeurs/groupe_instructeurs/show.html.haml @@ -47,4 +47,4 @@ params: { instructeur: { id: instructeur.id }}, class: 'button' } - = paginate @instructeurs + = paginate @instructeurs, views_prefix: 'shared' diff --git a/app/views/instructeurs/procedures/deleted_dossiers.html.haml b/app/views/instructeurs/procedures/deleted_dossiers.html.haml index 32d81df04..d82db343c 100644 --- a/app/views/instructeurs/procedures/deleted_dossiers.html.haml +++ b/app/views/instructeurs/procedures/deleted_dossiers.html.haml @@ -34,7 +34,7 @@ %li L’utilisateur a intentionnellement supprimé son dossier. %li Le délai de conservation maximal de #{@procedure.duree_conservation_dossiers_dans_ds} mois a expiré. Conformément au règlement RGPD, DS ne peut continuer à les héberger. - if @deleted_dossiers.any? - = paginate @deleted_dossiers + = paginate @deleted_dossiers, views_prefix: 'shared' %table.table.dossiers-table.hoverable %thead %tr @@ -53,6 +53,6 @@ = deletion_reason_badge(deleted_dossier.reason) %td.deleted-cell = l(deleted_dossier.deleted_at, format: '%d/%m/%y') - = paginate @deleted_dossiers + = paginate @deleted_dossiers, views_prefix: 'shared' - else Aucun dossier supprimé diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 9796192d1..a780d57f1 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -184,7 +184,8 @@ turbo: false } %tfoot %tr - %td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 }= paginate @filtered_sorted_paginated_ids + %td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 } + = paginate @filtered_sorted_paginated_ids, views_prefix: 'shared' - else %h2.empty-text diff --git a/app/views/recherche/index.html.haml b/app/views/recherche/index.html.haml index 2057de441..d98aeef72 100644 --- a/app/views/recherche/index.html.haml +++ b/app/views/recherche/index.html.haml @@ -11,7 +11,7 @@ = t('pluralize.dossier_trouve', count: @dossiers_count) - if @projected_dossiers.present? - = paginate @paginated_ids + = paginate @paginated_ids, views_prefix: 'shared' .fr-table.fr-table--bordered %table.table.dossiers-table.hoverable %thead @@ -105,7 +105,7 @@ - else %td - = paginate @paginated_ids + = paginate @paginated_ids, views_prefix: 'shared' - else %h2 Aucun dossier correspondant à votre recherche n’a été trouvé diff --git a/app/views/administrateurs/kaminari/_first_page.html.haml b/app/views/shared/kaminari/_first_page.html.haml similarity index 100% rename from app/views/administrateurs/kaminari/_first_page.html.haml rename to app/views/shared/kaminari/_first_page.html.haml diff --git a/app/views/administrateurs/kaminari/_gap.html.haml b/app/views/shared/kaminari/_gap.html.haml similarity index 100% rename from app/views/administrateurs/kaminari/_gap.html.haml rename to app/views/shared/kaminari/_gap.html.haml diff --git a/app/views/administrateurs/kaminari/_last_page.html.haml b/app/views/shared/kaminari/_last_page.html.haml similarity index 100% rename from app/views/administrateurs/kaminari/_last_page.html.haml rename to app/views/shared/kaminari/_last_page.html.haml diff --git a/app/views/administrateurs/kaminari/_next_page.html.haml b/app/views/shared/kaminari/_next_page.html.haml similarity index 100% rename from app/views/administrateurs/kaminari/_next_page.html.haml rename to app/views/shared/kaminari/_next_page.html.haml diff --git a/app/views/administrateurs/kaminari/_page.html.haml b/app/views/shared/kaminari/_page.html.haml similarity index 100% rename from app/views/administrateurs/kaminari/_page.html.haml rename to app/views/shared/kaminari/_page.html.haml diff --git a/app/views/administrateurs/kaminari/_paginator.html.haml b/app/views/shared/kaminari/_paginator.html.haml similarity index 82% rename from app/views/administrateurs/kaminari/_paginator.html.haml rename to app/views/shared/kaminari/_paginator.html.haml index 4c535c675..9c018cf5f 100644 --- a/app/views/administrateurs/kaminari/_paginator.html.haml +++ b/app/views/shared/kaminari/_paginator.html.haml @@ -1,5 +1,5 @@ = paginator.render do - %nav.fr-pagination{ role: 'navigation', 'aria-label': 'Pagination' } + %nav.fr-pagination.fr-mt-2w{ role: 'navigation', 'aria-label': 'Pagination' } %ul.fr-pagination__list = first_page_tag unless current_page.first? = prev_page_tag unless current_page.first? diff --git a/app/views/administrateurs/kaminari/_prev_page.html.haml b/app/views/shared/kaminari/_prev_page.html.haml similarity index 100% rename from app/views/administrateurs/kaminari/_prev_page.html.haml rename to app/views/shared/kaminari/_prev_page.html.haml diff --git a/app/views/users/dossiers/_deleted_dossiers_list.html.haml b/app/views/users/dossiers/_deleted_dossiers_list.html.haml index 23df15f00..b824fba22 100644 --- a/app/views/users/dossiers/_deleted_dossiers_list.html.haml +++ b/app/views/users/dossiers/_deleted_dossiers_list.html.haml @@ -27,7 +27,7 @@ %td = dossier.updated_at.strftime('%d/%m/%Y') - = paginate(deleted_dossiers) + = paginate deleted_dossiers, views_prefix: 'shared' - else .blank-tab diff --git a/app/views/users/dossiers/_dossiers_list.html.haml b/app/views/users/dossiers/_dossiers_list.html.haml index 26ba0d038..b62773984 100644 --- a/app/views/users/dossiers/_dossiers_list.html.haml +++ b/app/views/users/dossiers/_dossiers_list.html.haml @@ -43,7 +43,7 @@ %td.action-col.follow-col = render partial: 'dossier_actions', locals: { dossier: dossier } - = paginate(dossiers) + = paginate dossiers, views_prefix: 'shared' - else .blank-tab diff --git a/app/views/users/dossiers/_hidden_dossiers_list.html.haml b/app/views/users/dossiers/_hidden_dossiers_list.html.haml index 0cf10f3dd..0eaca0601 100644 --- a/app/views/users/dossiers/_hidden_dossiers_list.html.haml +++ b/app/views/users/dossiers/_hidden_dossiers_list.html.haml @@ -31,7 +31,7 @@ = link_to restore_dossier_path(dossier.id), method: :patch, class: "fr-btn" do Restaurer - = paginate(hidden_dossiers) + = paginate hidden_dossiers, views_prefix: 'shared' - else .blank-tab diff --git a/app/views/users/dossiers/_transfered_dossiers_list.html.haml b/app/views/users/dossiers/_transfered_dossiers_list.html.haml index 9774e7253..f3aff1b8e 100644 --- a/app/views/users/dossiers/_transfered_dossiers_list.html.haml +++ b/app/views/users/dossiers/_transfered_dossiers_list.html.haml @@ -28,7 +28,7 @@ %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) + = paginate dossier_transfers, views_prefix: 'shared' - else .blank-tab From 995858e1246881cd96d4d1e9e4f2568a73a09175 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 24 Apr 2023 09:45:36 +0200 Subject: [PATCH 70/75] fix defaut_groupe_instructeur_id for (hidden) procedure with an (hidden) parent --- ..._defaut_groupe_instructeur_id_for_cloned_procedure.rake | 7 ++++++- ...ut_groupe_instructeur_id_for_cloned_procedure_spec.rake | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake index 16fef0c4a..ed07c911e 100644 --- a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake +++ b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake @@ -1,7 +1,9 @@ namespace :after_party do desc 'Deployment task: fix_defaut_groupe_instructeur_id_for_cloned_procedure' task fix_defaut_groupe_instructeur_id_for_cloned_procedure: :environment do + # rubocop:disable DS/Unscoped procedures = Procedure + .unscoped .joins(:groupe_instructeurs) .where.not(parent_procedure_id: nil) .where("procedures.created_at > ?", Time.zone.parse("17/04/2023")) @@ -9,13 +11,16 @@ namespace :after_party do procedures.each do |p| if !p.defaut_groupe_instructeur_id.in?(p.groupe_instructeurs.map(&:id)) wrong_groupe = p.defaut_groupe_instructeur - new_defaut_groupe = p.groupe_instructeurs.find_by(label: p.parent_procedure.defaut_groupe_instructeur.label) + parent_procedure = Procedure.unscoped.find(p.parent_procedure_id) + new_defaut_groupe = p.groupe_instructeurs.find_by(label: parent_procedure.defaut_groupe_instructeur.label) p.update!(defaut_groupe_instructeur: new_defaut_groupe) p.dossiers.where(groupe_instructeur: wrong_groupe).update_all(groupe_instructeur_id: new_defaut_groupe.id) end end + # rubocop:enable DS/Unscoped + AfterParty::TaskRecord .create version: AfterParty::TaskRecorder.new(__FILE__).timestamp end diff --git a/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake b/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake index f5add3d77..3e9440db0 100644 --- a/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake +++ b/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake @@ -1,8 +1,8 @@ describe '20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure' do let(:rake_task) { Rake::Task['after_party:fix_defaut_groupe_instructeur_id_for_cloned_procedure'] } - let!(:parent_procedure) { create(:procedure) } - let(:procedure) { create(:procedure, parent_procedure:) } + let!(:parent_procedure) { create(:procedure, hidden_at: Time.zone.now) } + let(:procedure) { create(:procedure, parent_procedure:, hidden_at: Time.zone.now) } let(:dossier) { create(:dossier, procedure:) } subject(:run_task) { rake_task.invoke } From c1891157574e9963f88e464f8a909ad4b69b0cde Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 24 Apr 2023 10:15:04 +0200 Subject: [PATCH 71/75] fix defaut_groupe_instructeur for mismatching label --- ...e_instructeur_id_for_cloned_procedure.rake | 4 +++- ...tructeur_id_for_cloned_procedure_spec.rake | 22 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake index ed07c911e..60bb0766e 100644 --- a/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake +++ b/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure.rake @@ -12,7 +12,9 @@ namespace :after_party do if !p.defaut_groupe_instructeur_id.in?(p.groupe_instructeurs.map(&:id)) wrong_groupe = p.defaut_groupe_instructeur parent_procedure = Procedure.unscoped.find(p.parent_procedure_id) - new_defaut_groupe = p.groupe_instructeurs.find_by(label: parent_procedure.defaut_groupe_instructeur.label) + new_defaut_groupe = p.groupe_instructeurs + .find_by(label: parent_procedure.defaut_groupe_instructeur.label).presence || p.groupe_instructeurs.first + p.update!(defaut_groupe_instructeur: new_defaut_groupe) p.dossiers.where(groupe_instructeur: wrong_groupe).update_all(groupe_instructeur_id: new_defaut_groupe.id) diff --git a/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake b/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake index 3e9440db0..51d3bcc3b 100644 --- a/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake +++ b/spec/lib/tasks/deployment/20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure_spec.rake @@ -3,24 +3,36 @@ describe '20230421091957_fix_defaut_groupe_instructeur_id_for_cloned_procedure' let!(:parent_procedure) { create(:procedure, hidden_at: Time.zone.now) } let(:procedure) { create(:procedure, parent_procedure:, hidden_at: Time.zone.now) } + let(:procedure_with_new_groupe) do + create(:procedure, parent_procedure:, hidden_at: Time.zone.now).tap do |p| + p.groupe_instructeurs.first.update!(label: 'a new label') + end + end let(:dossier) { create(:dossier, procedure:) } subject(:run_task) { rake_task.invoke } after(:each) { rake_task.reenable } it 'populates defaut_groupe_instructeur_id' do - procedure.update_columns(defaut_groupe_instructeur_id: parent_procedure.defaut_groupe_instructeur_id) + [procedure, procedure_with_new_groupe].each do |p| + defaut_groupe_instructeur_id = parent_procedure.defaut_groupe_instructeur_id + p.update_columns(defaut_groupe_instructeur_id:) + end dossier - expect(procedure.defaut_groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be false + expect(procedure_has_defaut_groupe?(procedure)).to be false expect(dossier.groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be false run_task - procedure.reload - dossier.reload + [procedure, procedure_with_new_groupe, dossier].each(&:reload) - expect(procedure.defaut_groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be true + expect(procedure_has_defaut_groupe?(procedure)).to be true + expect(procedure_has_defaut_groupe?(procedure_with_new_groupe)).to be true expect(dossier.groupe_instructeur_id.in?(procedure.groupe_instructeurs.ids)).to be true end + + def procedure_has_defaut_groupe?(p) + p.defaut_groupe_instructeur_id.in?(p.groupe_instructeurs.ids) + end end From 75415cf6dc3c18e8a6c76df0fe68be0422faa8a2 Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Mon, 24 Apr 2023 10:26:03 +0200 Subject: [PATCH 72/75] Fix typo issue Co-authored-by: Colin Darie --- app/views/administrateurs/procedures/all.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/administrateurs/procedures/all.html.haml b/app/views/administrateurs/procedures/all.html.haml index 0032d085f..55ba8bb0d 100644 --- a/app/views/administrateurs/procedures/all.html.haml +++ b/app/views/administrateurs/procedures/all.html.haml @@ -38,7 +38,7 @@ - if @filter.from_publication_date.present? .selected-from-publication-date.fr-mb-2w = link_to "Depuis #{l(@filter.from_publication_date)}", all_admin_procedures_path(@filter.without(:from_publication_date)), class: 'fr-tag fr-tag--dismiss fr-mb-1w' - = paginate @procedures, params: @filter.params, , views_prefix: 'shared' + = paginate @procedures, params: @filter.params, views_prefix: 'shared' %thead %tr %th{ scope: 'col' } From 8c78008b4d08e135ea70d84b4b76b088f2ad0650 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 24 Apr 2023 10:38:17 +0200 Subject: [PATCH 73/75] =?UTF-8?q?correctif(usager.depot-dossier):=20ETQ=20?= =?UTF-8?q?usager,=20les=20titres=20de=20section=20conditionn=C3=A9s=20eta?= =?UTF-8?q?it=20visible=20meme=20si=20la=20condition=20n'etait=20pas=20rem?= =?UTF-8?q?plie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editable_champ/header_section_component.rb | 9 +++++---- .../header_section_component.html.haml | 2 +- spec/system/users/brouillon_spec.rb | 5 +++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/components/editable_champ/header_section_component.rb b/app/components/editable_champ/header_section_component.rb index 253e32ff0..696f20110 100644 --- a/app/components/editable_champ/header_section_component.rb +++ b/app/components/editable_champ/header_section_component.rb @@ -12,10 +12,11 @@ class EditableChamp::HeaderSectionComponent < ApplicationComponent end def header_section_classnames - class_names = ["fr-h#{level}"] - - class_names << 'header-section' if @champ.dossier.auto_numbering_section_headers_for?(@champ) - class_names + class_names( + "fr-h#{level}": true, + 'header-section': @champ.dossier.auto_numbering_section_headers_for?(@champ), + 'hidden': !@champ.visible? + ) end def tag_for_depth diff --git a/app/components/editable_champ/header_section_component/header_section_component.html.haml b/app/components/editable_champ/header_section_component/header_section_component.html.haml index 9f3efef55..a16a46fdc 100644 --- a/app/components/editable_champ/header_section_component/header_section_component.html.haml +++ b/app/components/editable_champ/header_section_component/header_section_component.html.haml @@ -1,2 +1,2 @@ -= tag.send(tag_for_depth, class: header_section_classnames) do += tag.send(tag_for_depth, class: header_section_classnames, id: @champ.input_group_id) do = libelle diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 95c2d5467..12eb2aaf8 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -406,6 +406,7 @@ describe 'The user' do types_de_champ_public: [ { type: :integer_number, libelle: 'age', stable_id: age_stable_id }, { type: :yes_no, libelle: 'permis de conduire', stable_id: permis_stable_id, condition: permis_condition }, + { type: :header_section, libelle: 'info voiture', condition: permis_condition }, { type: :integer_number, libelle: 'tonnage', stable_id: tonnage_stable_id, condition: tonnage_condition }, { type: :text, libelle: 'parking', condition: parking_condition } ]) @@ -418,16 +419,20 @@ describe 'The user' do expect(page).to have_css('label', text: 'age', visible: true) expect(page).to have_no_css('label', text: 'permis de conduire', visible: true) + expect(page).to have_no_css('legend h2', text: 'info voiture', visible: true) expect(page).to have_no_css('label', text: 'tonnage', visible: true) fill_in('age', with: '18') expect(page).to have_css('label', text: 'permis de conduire', visible: true) + expect(page).to have_css('legend h2', text: 'info voiture', visible: true) expect(page).to have_no_css('label', text: 'tonnage', visible: true) + choose('Oui') expect(page).to have_css('label', text: 'permis de conduire', visible: true) expect(page).to have_css('label', text: 'tonnage', visible: true) + fill_in('tonnage', with: '1') expect(page).to have_css('label', text: 'parking', visible: true) From 402112583d57d8368d2bc27ccc5ce47cc986a0be Mon Sep 17 00:00:00 2001 From: Lisa Durand Date: Mon, 24 Apr 2023 10:53:18 +0200 Subject: [PATCH 74/75] fix test with good classname in js for batch --- app/javascript/controllers/batch_operation_controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/batch_operation_controller.ts b/app/javascript/controllers/batch_operation_controller.ts index abc294b9c..dcb719dc5 100644 --- a/app/javascript/controllers/batch_operation_controller.ts +++ b/app/javascript/controllers/batch_operation_controller.ts @@ -20,7 +20,7 @@ export class BatchOperationController extends ApplicationController { this.inputTargets.forEach((e) => (e.checked = target.checked)); this.toggleSubmitButtonWhenNeeded(); - const pagination = document.querySelector('tfoot .pagination'); + const pagination = document.querySelector('tfoot .fr-pagination'); if (pagination) { displayNotice(this.inputTargets); } From ae1fd3f30bf28041e713a5788766b315e6dd5871 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 24 Apr 2023 10:59:42 +0200 Subject: [PATCH 75/75] linter --- spec/system/users/brouillon_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 12eb2aaf8..91bf885f8 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -427,12 +427,10 @@ describe 'The user' do expect(page).to have_css('legend h2', text: 'info voiture', visible: true) expect(page).to have_no_css('label', text: 'tonnage', visible: true) - choose('Oui') expect(page).to have_css('label', text: 'permis de conduire', visible: true) expect(page).to have_css('label', text: 'tonnage', visible: true) - fill_in('tonnage', with: '1') expect(page).to have_css('label', text: 'parking', visible: true)